Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // Edistynyt mobiiliohjelmointi 17.4.2023
- // edellisellä luennolla tehtiin valmiiksi kaksi fragmentia:
- - FeedbackReadFragment
- - FeedbackSendFragment
- Pohjat näille löytyvät edellisen luennon muistiinpanoista.
- // koska Directus on omalla koneella, muutetaan AndroidManifestia niin että suojaamaton yhteys on sallittu:
- // eli lisätään android:usesCleartextTraffic="true" application-tägin sisälle
- <application
- android:usesCleartextTraffic="true"
- android:allowBackup="true"
- android:dataExtractionRules="@xml/data_extraction_rules"
- jne
- // pelkkä datan hakeminen on hyvin samanlaista kuin harjoituksessa 1 (Commment-data)
- class FeedbackReadFragment : Fragment() {
- private var _binding: FragmentFeedbackReadBinding? = 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 = FragmentFeedbackReadBinding.inflate(inflater, container, false)
- val root: View = binding.root
- // haetaan feedback-data Directusista heti kun ladataan fragment
- getFeedbacks()
- return root
- }
- fun getFeedbacks() {
- // 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
- // tämä olisi hyvä olla myös local.propertiesissa
- // esim. DIRECTUS_ADDRESS
- // koodissa haetaan BuildConfig.DIRECTUS_ADDRESS
- // muista tällöin käynnistää sovellus kerran että BuildConfig päivittyy
- // huomaa että access token on jokaisella omansa
- // myös portin pitää olla oikea, tässä tapauksessa 8055
- // ip osoite kuten 10.0.2.2 viittaa tietokoneeseen
- // ja portti 8055 viittaa tiettyyn sovellukseen tietokoneessa, eli Directusiin
- // 10.0.2.2 on virtuaaliosoite, joka osoittaa tietokoneen localhostiin
- // pelkkä localhost viittaisi tässä nyt Android-emulaattoriin
- val JSON_URL = "http://10.0.2.2:8055/items/feedback?access_token=OMA_ACCESS_TOKEN_TÄHÄN"
- // Request a string response from the provided URL.
- val stringRequest: StringRequest = object : StringRequest(
- Request.Method.GET, JSON_URL,
- Response.Listener { response ->
- // näytetään raakadata LogCatissa
- 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"
- 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
- }
- }
- // kun otetaan myös GSON mukaan, voidaan tehdä funktiossa näin:
- fun getFeedbacks() {
- // alustava lista Feedback-datalle + GSONin alustus
- var feedbacks : List<Feedback> = emptyList()
- val gson = GsonBuilder().setPrettyPrinting().create()
- // tämä olisi hyvä olla myös local.propertiesissa
- // esim. DIRECTUS_ADDRESS
- // koodissa haetaan BuildConfig.DIRECTUS_ADDRESS
- // muista tällöin käynnistää sovellus kerran että BuildConfig päivittyy
- // huomaa että access token on jokaisella omansa
- // myös portin pitää olla oikea, tässä tapauksessa 8055
- // ip osoite kuten 10.0.2.2 viittaa tietokoneeseen
- // ja portti 8055 viittaa tiettyyn sovellukseen tietokoneessa, eli Directusiin
- // 10.0.2.2 on virtuaaliosoite, joka osoittaa tietokoneen localhostiin
- // pelkkä localhost viittaisi tässä nyt Android-emulaattoriin
- val JSON_URL = "http://10.0.2.2:8055/items/feedback?access_token=zescNylxyUjOGBTfXrSYgysW52EbspyU"
- // Request a string response from the provided URL.
- val stringRequest: StringRequest = object : StringRequest(
- Request.Method.GET, JSON_URL,
- Response.Listener { response ->
- // näytetään raakadata LogCatissa
- Log.d("TESTI", response)
- // koska Directus tallentaa varsinaisen datan kenttään "data", pitää meidän
- // suodattaa alkuperäistä JSONia hieman
- val jObject = JSONObject(response)
- val jArray = jObject.getJSONArray("data")
- // muutetaan lista Feedback-dataa Kotlin-dataksi
- feedbacks = gson.fromJson(jArray.toString() , Array<Feedback>::class.java).toList()
- // testataan toimiiko data oikeasti -> silmukalla sijainnit LogCatiin
- for(item : Feedback in feedbacks) {
- Log.d("TESTI", item.location.toString())
- }
- },
- 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
- }
- }
- // 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)
- }
- // kytketään ulkkoasussa olevaan ListViewiin
- // ks. tämän osalta edellisen luennon muistiinpanot
- Response.Listener { response ->
- // näytetään raakadata LogCatissa
- Log.d("TESTI", response)
- // koska Directus tallentaa varsinaisen datan kenttään "data", pitää meidän
- // suodattaa alkuperäistä JSONia hieman
- val jObject = JSONObject(response)
- val jArray = jObject.getJSONArray("data")
- // muutetaan lista Feedback-dataa Kotlin-dataksi
- feedbacks = gson.fromJson(jArray.toString() , Array<Feedback>::class.java).toList()
- // testataan toimiiko data oikeasti -> silmukalla sijainnit LogCatiin
- for(item : Feedback in feedbacks) {
- Log.d("TESTI", item.location.toString())
- }
- // luodaan adapteri ListViewille, voit korvata tämän RecyclerViewillä myös!
- // Huom: tämä ei toimi suoraan, johtuen ListViewin vaatimuksesta
- // käyttää vain Stringejä, ks. punainen kommentti alempaa
- val adapter = ArrayAdapter(activity as Context, R.layout.simple_list_item_1, feedbacks)
- // muista myös lisätä ListView fragmentin ulkoasuun (xml)
- // ListView löytyy Design-valikosta otsikon "Legacy" alta
- binding.listViewFeedbacks.adapter = adapter
- }
- // oletuksena ListView näyttää olion sisältä kaiken sisällön, koska ListView tukee vain String-muotoisia listoja.
- // helppoin tapa hoitaa tämä on yliajaa Feedback-luokan toString()-metodi, joka päättää mitä tulostetaan:
- data class Feedback (
- @SerializedName("id" ) var id : Int? = null,
- @SerializedName("name" ) var name : String? = null,
- @SerializedName("location" ) var location : String? = null,
- @SerializedName("value" ) var value : String? = null
- )
- {
- // yliajetaan luokan toString-metodi
- // jotta ListView käyttää tätä Feedbackin tulostamiseen
- // muutoin ListView tulostaa kaikki sisällöt, mikä näyttää huonolta
- // ja saattaa muutenkin näyttää liikaa tietoa käyttöliittymässä
- override fun toString(): String {
- return "${name}: ${location}"
- }
- }
- // DATAN LÄHETTÄMINEN ANDROIDISSA DIRECTUSIIN
- // testataan EditTextit läpi ensin:
- class FeedbackSendFragment : Fragment() {
- 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 jokainen EditTextin sisältö talteen muuttujiin
- binding.buttonSendFeedback.setOnClickListener {
- val name = binding.editTextFeedbackName.text.toString()
- val location = binding.editTextFeedbackLocation.text.toString()
- val value = binding.editTextFeedbackValue.text.toString()
- // tulostetaan testimielessä lokiin kaikki
- Log.d("TESTI", "${name} - ${location} - ${value}")
- }
- return root
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- }
- // tehdään FeedbackSendFragment uusi apufunktio, jonka tehtävänä on lähettää uusi feedback Directusiin
- // tehdään apufunktio, jonka avulla voidaan lähettää uusi Feedback Directusiin
- // käytännössä tämän sisälle tulee Volley-koodi, mutta POST-versio
- fun sendFeedback(name : String, location : String, value : String) {
- }
- // tämän jälkeen pitää muistaa myös kutsua tätä funktiota Buttonin kautta kun tiedot on saatu haettua:
- // kun nappia painetaan, haetaan jokainen EditTextin sisältö talteen muuttujiin
- binding.buttonSendFeedback.setOnClickListener {
- val name = binding.editTextFeedbackName.text.toString()
- val location = binding.editTextFeedbackLocation.text.toString()
- val value = binding.editTextFeedbackValue.text.toString()
- // tulostetaan testimielessä lokiin kaikki
- Log.d("TESTI", "${name} - ${location} - ${value}")
- sendFeedback(name, location, value)
- }
- // Volley-koodissa getBody() -metodi pitää ohjelmoida siten, että siinä rakennetaan uusi data Directusia varten
- // 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
- // rakennetaan Feedback-olio lennosta, ja muutetaan se GSONin avulla tekstimuotoon, eli String
- var f : Feedback = Feedback()
- f.location = location
- f.name = name
- f.value = value
- // muutetaan Feedback-olio -> JSONiksi
- var gson = GsonBuilder().create()
- var newData = gson.toJson(f)
- body = newData.toByteArray(Charsets.UTF_8)
- } catch (e: UnsupportedEncodingException) {
- // problems with converting our data into UTF-8 bytes
- }
- return body
- }
- // Jos halutaan käyttäjästä huolehtia sen jälkeen kuin uusi Feedback meni onnistuneesti perille, ks. Response-koodi FeedbackSendFragmentin Volley-koodissa:
- Response.Listener { response ->
- Log.d("TESTI", "Uusi feedback tallennettu!")
- // idea 1: tyhjennetään EditTextit jotta käyttäjä huomaa että jotain tapahtui
- binding.editTextFeedbackLocation.setText("")
- binding.editTextFeedbackName.setText("")
- binding.editTextFeedbackValue.setText("")
- // idea 2: näytetään ponnahdusviesti (Toast-viesti) että viesti on lähtenyt
- // yleensä jos context ei toimi, kokeile: activity as Context
- Toast.makeText(context, "Thank you for your feedback!", Toast.LENGTH_LONG).show()
- // idea 3: siirretään käyttäjä automaattisesti FeedbackReadFragmentiin
- // jotta hän näkee oman uuden viestinsä
- // mobile_navigation -> tehdään action FeedbackSendFragmentista FeedbackReadFragmentiin
- // ei parametreja
- // ja tässä välissä ohjataan findNavControllerin avulla käyttäjä takaisin FeedbackReadFragmentiin
- // Note: if you send data to API instead, this might not be needed
- },
- // Basic Auth on hyvin yksinkertainen suojaus rajapintaan, mikä ei kuitenkaan tänä päivänä ole enää riittävä
- // siläl Base64-tekstin saa käännettyä molempiin suuntiin
- // 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("TESTI", 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"
- // replace with your own API's login info
- // basic authissa pitää rakentaa Authorization -teksti erikseen
- // muodosssa username:password , ja käännettynä Bsae64-formaattiin
- val authorizationString = "Basic " + Base64.encodeToString(
- ("admin" + ":" + "12345").toByteArray(), Base64.DEFAULT
- )
- 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)
- // TEMP ACCESS TOKENIN KÄYTTÄMINEN
- class TempAccessFragment : Fragment() {
- private var _binding: FragmentTempAccessBinding? = null
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- // 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 oma Singleton-luokka.
- // ks. Google ja Volleyn dokumentaatio esimerkistä miten tämä tehdään.
- // seuraavat laitettu local.propertiesiin
- // muista käyttää ohjelmaa päällä jotta BuildConfig päivittyy
- // DIRECTUS_HOST=http://10.0.2.2:8055
- // DIRECTUS_EMAIL=joku@jossain.com
- // DIRECTUS_PASSWORD=Password123!
- // VARIABLES USED BY THE SESSION MANAGEMENT
- val LOGIN_URL = BuildConfig.DIRECTUS_HOST + "/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 = BuildConfig.DIRECTUS_HOST
- // if using username + password in the service (e.g. Directus), use these
- val username = BuildConfig.DIRECTUS_EMAIL
- 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
- binding.buttonGetRawData.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()
- }
- // button methods
- fun loginAction()
- {
- Log.d("ADVTECH", "login")
- Log.d("ADVTECH", JSON_URL + " login")
- requestQueue?.add(loginRequest)
- }
- 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() {
- if (loggedIn) {
- requestQueue?.add(dataRequest)
- }
- }
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
- // REQUEST OBJECTS
- // REQUEST OBJECT 1: LOGIN
- var loginRequest: StringRequest = object : StringRequest(
- Request.Method.POST, LOGIN_URL,
- Response.Listener { response ->
- var responseJSON: JSONObject = JSONObject(response)
- // save the refresh token too if using refresh logic
- // refreshToken = responseJSON.get("refresh_token").toString()
- // this part depends completely on the service that is used
- // Directus uses the data -> access_token -approach
- // IBM Cloud handles the version in comments
- // accessToken = responseJSON.get("access_token").toString()
- 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"
- // IBM Cloud expects the Content-Type to be the following:
- // headers["Content-Type"] = "application/x-www-form-urlencoded"
- // 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+"/items/feedback",
- Response.Listener { response ->
- Log.d("ADVTECH", response)
- binding.textViewTempFeedbackRaw.text = response
- },
- Response.ErrorListener {
- // typically this is a connection error
- Log.d("ADVTECH", it.toString())
- 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>()
- // headers["Accept"] = "application/json"
- // headers["Content-Type"] = "application/json; charset=utf-8"
- headers["Authorization"] = "Bearer " + accessToken
- return headers
- }
- }
- }
Add Comment
Please, Sign In to add comment