Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // Edistynyt mobiiliohjelmointi, 3.3.2023
- 1. Tehdään ensiksi mobile_navigationin avulla uusi fragment nimeltä FeedbackSendFragment
- (ei lisätä päävalikkoon)
- 2. Lisää mobile_navigationissa action:
- FeedbackReadFragment -> FeedbackSendFragment
- (ei argumentteja)
- 3. Lopuksi lisätään Button FeedbackReadFragmentiin (ListViewin päälle esim.), jota klikattaessa ohjelma siirtyy FeedbackSendFragmentiin
- // napilla siirtyminen FeedbackReadFragmentista -> FeedbackSendFragmentiin
- // ulkoasu-xml (FeedbackReadFragment)
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- tools:context=".FeedbackReadFragment">
- <Button
- android:id="@+id/button_send_feedback_fragment"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Send feedback" />
- <ListView
- android:id="@+id/listView_feedbacks"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </LinearLayout>
- // muutetaan FeedbackReadFragment.kt:ssa onCreateViewiä:
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentFeedbackReadBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // navigoidaan napista FeedbackSendFragmentiin
- binding.buttonSendFeedbackFragment.setOnClickListener {
- val action = FeedbackReadFragmentDirections.actionFeedbackReadFragmentToFeedbackSendFragment()
- it.findNavController().navigate(action)
- }
- getFeedbacks()
- return root
- }
- // FeedbackSendFragmentin xml-ulkoasu, esimerkkilomake uutta Feedbackia varten
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:layout_margin="10dp"
- tools:context=".FeedbackSendFragment">
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:text="Send us feedback!"
- android:textColor="#239A5D"
- android:textSize="24sp"
- android:textStyle="bold" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Your name"
- android:textStyle="bold" />
- <EditText
- android:id="@+id/editText_feedback_name"
- android:layout_width="match_parent"
- android:layout_height="48dp"
- android:ems="10"
- android:hint="Write your name here"
- android:inputType="textPersonName" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Your location"
- android:textStyle="bold" />
- <EditText
- android:id="@+id/editText_feedback_location"
- android:layout_width="match_parent"
- android:layout_height="48dp"
- android:ems="10"
- android:hint="Where are you?"
- android:inputType="textPersonName" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Your feedback"
- android:textStyle="bold" />
- <EditText
- android:id="@+id/editText_feedback_value"
- android:layout_width="match_parent"
- android:layout_height="48dp"
- android:ems="10"
- android:hint="How did we do today?"
- android:inputType="textPersonName" />
- <Button
- android:id="@+id/button_send_feedback"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="20dp"
- android:text="SEND FEEDBACK" />
- </LinearLayout>
- // FeedbackSendFragment.kt , otetaan binding layer käyttöön:
- class FeedbackSendFragment : Fragment() {
- // change this to match your fragment name
- private var _binding: FragmentFeedbackSendBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentFeedbackSendBinding.inflate(inflater, container, false)
- val root: View = binding.root
- return root
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
- // FeedbackSendFragment, kokeillaan hakea napilla arvot edittexteistä
- class FeedbackSendFragment : Fragment() {
- // change this to match your fragment name
- private var _binding: FragmentFeedbackSendBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentFeedbackSendBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // kun nappia painetaan, haetaan edittexteistä arvot, ja kutsutaan apufunktiota
- binding.buttonSendFeedback.setOnClickListener{
- var name = binding.editTextFeedbackName.text.toString()
- var location = binding.editTextFeedbackLocation.text.toString()
- var value = binding.editTextFeedbackValue.text.toString()
- Log.d("TESTI", "${name} - ${location} - ${value}")
- }
- return root
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
- // FeedbackSendFragment, Volley-koodin kanssa
- class FeedbackSendFragment : Fragment() {
- // change this to match your fragment name
- private var _binding: FragmentFeedbackSendBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentFeedbackSendBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // kun nappia painetaan, haetaan edittexteistä arvot, ja kutsutaan apufunktiota
- binding.buttonSendFeedback.setOnClickListener{
- var name = binding.editTextFeedbackName.text.toString()
- var location = binding.editTextFeedbackLocation.text.toString()
- var value = binding.editTextFeedbackValue.text.toString()
- Log.d("TESTI", "${name} - ${location} - ${value}")
- sendFeedback(name, location, value)
- }
- return root
- }
- // apufunktio, jonka kautta uusi feedback lähetetään Volleyllä
- fun sendFeedback(name : String, location : String, value : String) {
- val JSON_URL = "https://xxxxxxxx.directus.app/items/feedback/?access_token=${BuildConfig.DIRECTUS_ACCESS_TOKEN}"
- // luodaan GSON-objekti
- var gson = GsonBuilder().create()
- // Request a string response from the provided URL.
- val stringRequest: StringRequest = object : StringRequest(
- Request.Method.POST, JSON_URL,
- Response.Listener { response ->
- Log.d("TESTI", "Directusiin lähetys ok!")
- // asetetaan kaikki edittextit tyhjiksi, jotta käyttäjä huomaa että jotain tapahtui
- binding.editTextFeedbackName.setText("")
- binding.editTextFeedbackLocation.setText("")
- binding.editTextFeedbackValue.setText("")
- // ilmoitetaan käyttäjälle myös Toast-viestillä että operaatio oli ok!
- Toast.makeText(context, "Thank you for your feedback!", Toast.LENGTH_SHORT).show()
- // koska activity.onBackPressed on deprecated, voidaan tehdä näin
- // eli palataan aiempaan fragmentiin.
- // tosin, näyttäisi siltä että tämä on hieman buginen, koska äppi
- // kaatuu mikäli yritetään heti perää siirtyä takaisin tähän frgamentiin lähettämään uutta feedbackia
- // activity?.getSupportFragmentManager()?.popBackStack()
- // parempi tapa -> tehdään action myös FeedbackSendFragmentista FeedbackReadFragmentiin
- // ja navigoidaan normaalisti takaisin aiempaan fragmentiin
- val action = FeedbackSendFragmentDirections.actionFeedbackSendFragmentToFeedbackReadFragment()
- findNavController().navigate(action)
- },
- Response.ErrorListener {
- // typically this is a connection error
- Log.d("ADVTECH", it.toString())
- })
- {
- @Throws(AuthFailureError::class)
- override fun getHeaders(): Map<String, String> {
- // we have to specify a proper header, otherwise Apigility will block our queries!
- // define we are after JSON data!
- val headers = HashMap<String, String>()
- headers["Accept"] = "application/json"
- headers["Content-Type"] = "application/json; charset=utf-8"
- return headers
- }
- // let's build the new data here
- @Throws(AuthFailureError::class)
- override fun getBody(): ByteArray {
- // this function is only needed when sending data
- var body = ByteArray(0)
- try { // check the example "Converting a Kotlin object to JSON"
- // on how to create this newData -variable
- var newData = ""
- // luodaan lennosta uusi Feedback-olio edittextien arvojen pohjalta
- var f : Feedback = Feedback()
- f.name = name
- f.location = location
- f.value = value
- // muutetaan Fedeback-olio -> JSON-muotoon GSONin avulla
- newData = gson.toJson(f);
- // JSON to bytes
- body = newData.toByteArray(Charsets.UTF_8)
- } catch (e: UnsupportedEncodingException) {
- // problems with converting our data into UTF-8 bytes
- }
- return body
- }
- }
- // Add the request to the RequestQueue. This has to be done in both getting and sending new data.
- val requestQueue = Volley.newRequestQueue(context)
- requestQueue.add(stringRequest)
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
- ##############################################################################################
- #### Harjoitukset 3 - Lisätehtävistä (Basic Auth ja temporary access tokenin käyttäminen) ####
- ##############################################################################################
- Basic Authin käyttäminen.
- Basic Auth ei itsessään ole kovin turvallinen, koska käyttäjänimi ja salasana "kryptataan" ainoastaan Base64-algoritmillä. Käytännössä Base64-algoritmin voi kääntää molempiin suuntiin, joten se ei ole varsinaisesti suojausalgoritmi. Toisin sanoen, jos joku saa kaapattua viestin, jossa Basic Auth tiedot ovat, se on helppo kääntää takaisin toisinpäin.
- Basic Auth vaatii seuraavanlaisen headerin, jotta se toimii:
- Authorization ---> Basic BASE64_MUODOSSA-OLEVA-USERNAME+SALASANA
- Ennen kuin käyttäjänimi ja salasana muutetaan Base64-muotoon, pitää ne asettaa samaan Stringiin, tällä tavalla (huomaa kaksoispiste välissä, erittäin tärkeä):
- username:password
- Eli jos username on admin ja salasana on 12345, silloin String olisi:
- admin:12345
- Muilta osin Volley-kutsu on täysin sama kuin aiemmin.
- Esim:
- class BasicAuthFragment : Fragment() {
- // change this to match your fragment name
- private var _binding: FragmentBasicAuthBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentBasicAuthBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // the binding -object allows you to access views in the layout, textviews etc.
- getUsers()
- return root
- }
- fun getUsers() {
- // this is the url where we want to get our data
- // Note: if using a local server, use http://10.0.2.2 for localhost. this is a virtual address for Android emulators, since
- // localhost refers to the Android device instead of your computer
- val JSON_URL = "https://apingweb.com/api/auth/users"
- // Request a string response from the provided URL.
- val stringRequest: StringRequest = object : StringRequest(
- Request.Method.GET, JSON_URL,
- Response.Listener { response ->
- Log.d("TESTI", response)
- },
- Response.ErrorListener {
- // typically this is a connection error
- Log.d("ADVTECH", it.toString())
- })
- {
- @Throws(AuthFailureError::class)
- override fun getHeaders(): Map<String, String> {
- // we have to specify a proper header, otherwise Apigility will block our queries!
- // define we are after JSON data!
- val headers = HashMap<String, String>()
- headers["Accept"] = "application/json"
- headers["Content-Type"] = "application/json; charset=utf-8"
- // tämä on ainoa muutos mikä pitää koodiin tehdä, jos rajapinta käyttää Basic Authia
- // --> Luodaan Authorization header, joka sisältää kirjautumistiedot Base64-formaatissa
- // replace with your own API's login info
- val authorizationString = "Basic " + Base64.encodeToString(
- ("admin" + ":" + "12345").toByteArray(), Base64.DEFAULT
- )
- Log.d("TESTI", authorizationString)
- headers.put("Authorization", authorizationString)
- return headers
- }
- }
- // Add the request to the RequestQueue. This has to be done in both getting and sending new data.
- val requestQueue = Volley.newRequestQueue(context)
- requestQueue.add(stringRequest)
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
- // Temporary access tokenin käyttäminen Androidissa, tätä varten tehty erillinen fragment päävalikkoon
- // nimeltä TempAccessFragment, ulkoasussa on textview ja button
- class TempAccessFragment : Fragment() {
- // HUOM! Tämä esimerkki on tehty hyvin pitkälle tyyliin "siitä mistä aita on matalin".
- // Jos haluat optimoida tätä rakennetta, ks. alla olevat kommentit
- // tällä hetkellä koodin logiikka on tämä:
- // jos datan hakemisessa tulee Auth-error -> kirjaudutaan kokonaan uudestaan rajapintaan.
- // tämäkin ratkaisu toimii (varsinkin pienillä käyttäjämäärillä), mutta tämän johdosta
- // Android-sovellus tekee paljon turhia kyselyjä Directusiin, koska kirjautuminen tehdään
- // aina virheen sattuessa tai fragmentin latautuessa uudelleen
- // tämä voi muodostua ongelmaksi, mikäli sovelluksella on tuhansia aktiivisia käyttäjiä.
- // tällaisessa tilanteessa jokainen säästetty ja optimoitu kysely Directusin rajapintaan
- // säästää Androidin käyttämää suoritusaikaa, akkua sekä myös Directusista tulevaa käyttölaskua.
- // Mitä vähemmän Android-sovellus "rassaa" Directusin rajapintaa, sen halvempi ja energiatehokkaampi
- // oma Android-sovellus on.
- // Parannusehdotus 1:
- // hyödynnetään refresh tokenia access tokenin uusimisessa (kevyempi operaatio kuin uudestaan kirjautuminen)
- // refresh token tulee samassa datassa kuin access token myös. Access token on 15min voimassa, ja refresh
- // token on 7 päivää voimassa. Refresh tokenin avulla voi pyytää uuden access tokenin, mikäli refresh token
- // on vielä voimassa. Jos myös refresh token vanhenee -> kirjaudutaan uudestaan. (tämä logiikka pitää koodata itse)
- // Parannusehdotus 2:
- // Directusin kirjautumisdatassa tulee mukana myös tieto siitä, kuinka kauan access token on voimassa kirjautumishetkestä
- // alkaen, oletuksena 900000 millisekuntia -> 900 sekuntia -> 15min
- // Voidaan koodata Android-sovellus siten, että niin kauan kuin aikaa on jäljellä, Directusiin ei lähetetä
- // yhtään kirjautumispyyntöä. Tällä tavalla Android-sovellus ei turhaan ole yhteydessä Directusiin,
- // koska äppi pitää itse kirjaa siitä milloin pitää kirjautua uusiksi.
- // Parannusehdotus 3:
- // kaikki kirjautumiseen liittyvä Volley-logiikka on hyvä keskittää yhteen paikkaan, joko kustomoituun
- // Application -luokkaan, tai tehdä (suositellumpi tapa) Volley-kutsuille om a Singleton-luokka.
- // ks. Google ja Volleyn dokumentaatio esimerkistä miten tämä tehdään.
- // change this to match your fragment name
- private var _binding: FragmentTempAccessBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- // VARIABLES USED BY THE SESSION MANAGEMENT
- val LOGIN_URL = "https://xxxxx.directus.app/auth/login"
- // these should be placed in the local properties file and used by BuildConfig
- // JSON_URL should be WITHOUT a trailing slash (/)!
- val JSON_URL = "https://xxxxx.directus.app/items/feedback"
- // if using username + password in the service (e.g. Directus), use these
- val username = BuildConfig.DIRECTUS_USERNAME
- val password = BuildConfig.DIRECTUS_PASSWORD
- // request queues for requests
- var requestQueue: RequestQueue? = null
- var refreshRequestQueue: RequestQueue? = null
- // state booleans to determine our session state
- var loggedIn: Boolean = false
- var needsRefresh: Boolean = false
- // stored tokens. refresh is used when our session token has expired
- // access token in this case is the same as session token
- var refreshToken = ""
- var accessToken = ""
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = FragmentTempAccessBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // napin kautta haetaan data uusiksi, jotta voidaan testata että vieläkö access token toimii
- binding.buttonDataAction.setOnClickListener {
- dataAction()
- }
- return root
- }
- // fragment entry point
- override fun onViewCreated(view: View, savedInstanceState: Bundle?)
- {
- super.onViewCreated(view, savedInstanceState);
- requestQueue = Volley.newRequestQueue(context)
- refreshRequestQueue = Volley.newRequestQueue(context)
- // start with login
- loginAction()
- }
- // kirjaudutaan Directusiin
- fun loginAction()
- {
- Log.d("ADVTECH", "login")
- Log.d("ADVTECH", JSON_URL + " login")
- requestQueue?.add(loginRequest)
- }
- // kirjaudutaan uusiksi jos refresh-muuttuja on tilassa True
- fun refreshLogin() {
- if (needsRefresh) {
- loggedIn = false
- // use this if using refresh logic
- // refreshRequestQueue?.add(loginRefreshRequest)
- // if using refresh logic, comment this line out
- loginAction()
- }
- }
- fun dataAction() {
- // haetaan dataa vain jos olemme vielä kirjautuneina sisään
- if (loggedIn) {
- requestQueue?.add(dataRequest)
- }
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- // REQUEST OBJECT 1: LOGIN
- var loginRequest: StringRequest = object : StringRequest(
- Request.Method.POST, LOGIN_URL,
- Response.Listener { response ->
- var responseJSON: JSONObject = JSONObject(response)
- // otetaan access token talteen (tässäkin voisi käyttää json2kt.com + GSON)
- // tämä esimerkki käyttää sisäänrakennettuja JSON-työkaluja
- accessToken = responseJSON.getJSONObject("data").get("access_token").toString()
- loggedIn = true
- // after login's done, get data from API
- dataAction()
- Log.d("ADVTECH", response)
- },
- Response.ErrorListener {
- // typically this is a connection error
- Log.d("ADVTECH", it.toString())
- }) {
- @Throws(AuthFailureError::class)
- override fun getHeaders(): Map<String, String> {
- // we have to provide the basic header info
- // + Bearer info => accessToken
- val headers = HashMap<String, String>()
- headers["Accept"] = "application/json"
- // for Directus, the typical approach works:
- headers["Content-Type"] = "application/json; charset=utf-8"
- return headers
- }
- // use this to build the needed JSON-object
- // this approach is used by Directus, IBM Cloud uses the commented version instead
- @Throws(AuthFailureError::class)
- override fun getBody(): ByteArray {
- // this function is only needed when sending data
- var body = ByteArray(0)
- try {
- // on how to create this newData -variable
- var newData = ""
- // a very quick 'n dirty approach to creating the needed JSON body for login
- newData = "{\"email\":\"${username}\", \"password\": \"${password}\"}"
- // JSON to bytes
- body = newData.toByteArray(Charsets.UTF_8)
- } catch (e: UnsupportedEncodingException) {
- // problems with converting our data into UTF-8 bytes
- }
- return body
- }
- }
- // REQUEST OBJECT 3 : ACTUAL DATA -> FEEDBACK
- var dataRequest: StringRequest = object : StringRequest(
- Request.Method.GET, JSON_URL,
- Response.Listener { response ->
- Log.d("ADVTECH", response)
- binding.textViewRawData.text = response
- },
- Response.ErrorListener {
- // typically this is a connection error
- Log.d("ADVTECH", it.toString())
- // jos tulee Auth-error -> kirjaudutaan uusiksi
- if (it is AuthFailureError) {
- Log.d("ADVTECH", "EXPIRED start")
- needsRefresh = true
- loggedIn = false
- refreshLogin()
- Log.d("ADVTECH", "EXPIRED end")
- }
- }) {
- @Throws(AuthFailureError::class)
- override fun getHeaders(): Map<String, String> {
- // we have to provide the basic header info
- // + Bearer info => accessToken
- val headers = HashMap<String, String>()
- // access tokenin täytyy kulkea headerseissa mukana
- // muutoin tulee virhe Directusin rajapinnasta (koska emme käytä enää
- // staattista tokenia)
- headers["Authorization"] = "Bearer " + accessToken
- return headers
- }
- }
- }
Add Comment
Please, Sign In to add comment