Úlohy pre 5. cvičenie

  • 1. DataBinding
  • 2. Jednosmerný, Obojsmerný DataBinding
  • 3. SharedPreferences
  • 4. OAuth autorizacia REST API poziadaviek s ulozenym tokenom
  • 5. Pokročilá autorizácia s Retrofit a OkHttpClient
  • 6. Ziskanie povolenia pre zistovanie GPS polohy
  • 7. Zobrazenie GPS polohy na mape

Hodnotenie

Za splnenie prvých 2 úloh celkovo 1 bod.

Za splnenie úlohy 3. celkovo 1 bod.

Za splnenie úloh 4 a 5 celkovo 1 bod.

Za splnenie úloh 6 a 7 celkovo 1 bod.

1. DataBinding

5.1.1 DataBinding v Android Aplikáciách

DataBinding je technika, ktorá umožňuje väzbu medzi vašimi UI komponentami v layout súborech a dátovými zdrojmi vo vašej aplikácii, použitím deklaratívneho formátu namiesto programovania. Táto knižnica ponúka spôsoby, ako aplikácii priradiť dáta tak, že UI komponenty nie sú naplnené dátami ručne v kóde.

  • Automatizácia aktualizácie UI: S DataBinding, aktualizácie vašich UI komponentov sú automatizované. Akonáhle sú dáta zmenené, zobrazenie sa aktualizuje automaticky, čo znižuje potrebu volania "setText()" alebo "findViewById()".
  • Zníženie množstva kódu: Pomáha redukovať množstvo kódu v aplikácii, čo vedie k zvýšeniu čitateľnosti a zníženiu pravdepodobnosti chýb.
  • Previazanie dát: Umožňuje modelovať dáta priamo s UI komponentmi v XML layout súbore, čo zjednodušuje prácu s dátami v UI.
  • Expression Language: Knižnica poskytuje vlastný jazyk pre výrazy (Expressions), ktorý umožňuje manipuláciu s dátami priamo v XML súbore.
  • Typová kontrola v čase kompilácie: Keďže väzby sú vyhodnotené v čase kompilácie, chyby sú ľahšie nájdené a opravené, pretože IDE ich môže identifikovať ešte pred spustením aplikácie.

Pre použitie DataBinding v projekte je potrebné pridať dataBinding do vášho build.gradle súboru. Potom môžete začať používať <layout> tag vo vašich XML layout súboroch a vytvoriť váš DataBinding objekt v kóde vašich aktivít alebo fragmentov.

build.gradle.kts

android {
    namespace = "eu.mcomputing.mobv.mobvzadanie"
    compileSdk = 33
	
	...
	
    buildFeatures {
        dataBinding = true
    }
	...
	

5.1.2 Použitie DataBinding v XML Layoutoch

DataBinding v Android aplikáciách vyžaduje špeciálnu štruktúru v XML layout súboroch. Táto štruktúra zahŕňa použitie <layout> tagu a vnoreného <data> tagu, ktorý umožňuje prepojenie dátových modelov s UI elementmi.

  • <layout> tag: Všetky XML layout súbory, ktoré chcú využívať DataBinding, musia byť obalené v <layout> tagu. Tento tag signalizuje, že layout bude používať databinding a umožňuje Android Studio generovať väzobné triedy.
  • <data> tag: Vnorený v <layout> tagu, <data> tag definuje dáta, ktoré budú použité v layoute. Môžete tu špecifikovať premenné, ktoré budú reprezentovať modely alebo logiku, ktorú chcete používať v UI.
  • Použitie premenných: Premenné definované v <data> tagu sa môžu používať priamo v layoute na nastavenie hodnôt UI komponentov alebo na manipuláciu s UI elementmi.
  • Automatická aktualizácia UI: Keď sú dáta v modeloch aktualizované, zmeny sa automaticky prejavia v príslušných UI komponentoch, bez nutnosti manuálneho zasahovania alebo volania metód ako setText() alebo findViewById().

Príklad použitia v XML layoute:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.MyViewModel" />
    </data>
    <-- Váš UI layout... -->
</layout>

Tento prístup umožňuje efektívne a prehľadne oddeliť logiku aplikácie od jej UI, čo zjednodušuje údržbu kódu a podporuje lepšiu organizáciu projektu.

5.1.3 Použitie DataBinding v Fragmentoch spolu s ViewModelom

DataBinding je mocný nástroj, ktorý poskytuje väzbu medzi UI komponentami v layoutoch a dátovými modelmi, logikou alebo ViewModelmi vo vašej aplikácii. V kombinácii s ViewModelom, DataBinding prispieva k udržateľnosti, testovateľnosti a zníženiu množstva boilerplate kódu vo vašich fragmentoch.

  • Inicializácia DataBinding: V rámci metódy onCreateView vo vašom fragmente sa inicializuje DataBinding. Tento proces zahŕňa inflatovanie layoutu fragmentu a následne pripojenie k ViewModelu.
  • Pripojenie k ViewModelu: Vytvoríte inštanciu ViewModelu pomocou ViewModelProvider. Potom nastavíte ViewModel pre váš DataBinding. Týmto sa ViewModel stane dostupným vo vašom XML layoute.
  • Aktualizácia dát: Keď sa dáta vo ViewModeli zmenia, UI sa automaticky aktualizuje vďaka observables alebo LiveData objektom, bez nutnosti manuálneho zasahovania.
  • Bezpečnostný aspekt: Použitie DataBindingu znižuje pravdepodobnosť vzniku chýb v runtime, ako sú NullPointerExceptions pri práci s UI komponentami, pretože väčšina týchto interakcií sa deje v compile-time.

Príklad použitia v kóde fragmentu:

		
class FeedFragment  : Fragment(R.layout.fragment_intro) {
    private var binding: FragmentFeedBinding? = null
	private lateinit var viewModel: FeedViewModel
	
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(requireActivity())[FeedViewModel::class.java]
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding = FragmentFeedBinding.bind(view).apply {
            lifecycleOwner = viewLifecycleOwner
            model = viewModel
        }.also { bnd->
            bnd.btnGenerate.setOnClickListener {
                viewModel.updateItems()
            }
        }
    }

    override fun onDestroyView() {
        binding = null
        super.onDestroyView()
    }
}

Tento prístup zjednodušuje kód vo fragmentoch, umožňuje lepšiu organizáciu a znižuje riziko chýb spojených s prácou s UI elementmi.

2. Jednosmerný, Obojsmerný DataBinding

5.2 Jednosmerný, Obojsmerný DataBinding a udalosti na kliknutia v XML Layoute

DataBinding v Android aplikáciách umožňuje priamu väzbu UI komponentov v XML layoutoch s dátovými zdrojmi v kóde aplikácie. Poskytuje možnosti pre jednosmerný (one-way) a obojsmerný (two-way) databinding, ako aj pridávanie udalosti na kliknutia priamo do XML.

Jednosmerný DataBinding

  • Definícia: Jednosmerný databinding aktualizuje UI komponenty keď sa zmenia dáta. Zmeny v UI neovplyvňujú dátové zdroje.
  • Použitie: Používa sa najmä pre zobrazenie informácií, kde interakcia používateľa nie je potrebná alebo nevedie k zmene dát.
  • Príklad: Môžete zobraziť meno používateľa zo ViewModelu v TextView pomocou @{viewModel.userName}.

Obojsmerný DataBinding

  • Definícia: Obojsmerný databinding umožňuje komunikáciu medzi UI a dátovým zdrojom v oboch smeroch. Ak sa zmení UI, zmení sa aj dátový zdroj a naopak.
  • Použitie: Ideálne pre formuláre alebo interaktívne rozhrania, kde zmeny vykonané používateľom musia byť odrážané v dátovom modeli.
  • Príklad: V prípade, že chcete, aby zmeny v EditText boli automaticky odrážané v vašom ViewModeli, použijete @={viewModel.userInput}.

Pridanie udalosti na kliknutia v XML

  • Definícia: DataBinding umožňuje definovať udalosti na kliknutia, priamo v XML layoute.
  • Použitie: Umožňuje vyvolanie metód z ViewModelu alebo akéhokoľvek pridruženého dátového zdroja priamo z UI elementu, bez potreby definovania listenera v Java alebo Kotlin kóde.
  • Príklad: Môžete pridať udalosti na kliknutia k tlačidlu takto: android:onClick="@{() -> viewModel.onButtonClick()}" alebo pre prenos parametrov android:onClick="@{(view) -> viewModel.onItemClick(item)}".

V oboch prípadoch databindingu a pri definovaní udalosti na kliknutia musíte vytvoriť vhodnú premennú alebo metódu v triede ViewModel alebo inom dátovom zdroji, ktorá sa použije pre databinding vo vašom XML. Taktiež musíte deklarovať databinding vo vašom XML layoute pomocou <layout> tagu a definovať premenné v jeho <data> sekcii.

Použitie jednosmerného a obojsmerného databindingu, spolu s definovaním udalosti na kliknutia priamo v XML, efektívne znižuje množstvo boilerplate kódu, zvyšuje čitateľnosť a udržateľnosť kódu a zjednodušuje prácu s UI aktualizáciami a interakciami používateľa.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="model"
            type="eu.mcomputing.mobv.mobvzadanie.viewmodels.AuthViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp">

        <EditText
            android:id="@+id/edit_text_username"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:hint="@string/zadajte_username"
            android:text="@={model.username}"
            app:layout_constraintTop_toBottomOf="@+id/label_username"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <Button
            android:id="@+id/submit_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/prihlasit_sa"
            android:onClick="@{()->model.loginUser()}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class AuthViewModel(private val dataRepository: DataRepository) : ViewModel() {
	...

    val username = MutableLiveData<String>()
    val email = MutableLiveData<String>()
    val password = MutableLiveData<String>()
    val repeat_password = MutableLiveData<String>()
	
	...
	
	fun loginUser() {
        viewModelScope.launch {
            val result = dataRepository.apiLoginUser(username.value?:"", password.value?:"")
            _loginResult.postValue(result.first ?: "")
            _userResult.postValue(result.second)
        }
    }
}

class LoginFragment : Fragment(R.layout.fragment_login) {

...

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding = FragmentLoginBinding.bind(view).apply {
            lifecycleOwner = viewLifecycleOwner
            model = viewModel
        }.also { bnd ->
            viewModel.loginResult.observe(viewLifecycleOwner) {
                if (it.isEmpty()) {
                    requireView().findNavController().navigate(R.id.action_login_feed)
                } else {
                    Snackbar.make(
                        bnd.submitButton,
                        it,
                        Snackbar.LENGTH_SHORT
                    ).show()
                }
            }

        }
    }
	
...
	
}

3. SharedPreferences

Android SharedPreferences

SharedPreferences je mechanizmus v systéme Android na ukladanie jednoduchých párov kľúč-hodnota a je bežne používaný na ukladanie primitívnych typov dát. Táto možnosť ukladania je vhodná na ukladanie malých množstiev dát, ako sú nastavenia používateľa, konfigurácie aplikácií alebo používateľskej relácie.

1. Inicializácia:

  • Prístup: K SharedPreferences môžete pristupovať prostredníctvom ľubovoľného kontextu v systéme Android (napríklad Activity, Service alebo BroadcastReceiver). Najčastejšie sa však používa metóda getSharedPreferences() alebo getPreferences().
  • Súbory: SharedPreferences ukladá dáta do súborov XML v priečinku vašej aplikácie.

class PreferenceData private constructor() {

    private fun getSharedPreferences(context: Context?): SharedPreferences? {
        return context?.getSharedPreferences(
            shpKey, Context.MODE_PRIVATE
        )
    }

    companion object {
        @Volatile
        private var INSTANCE: PreferenceData? = null

        private val lock = Any()

        fun getInstance(): PreferenceData =
            INSTANCE ?: synchronized(lock) {
                INSTANCE
                    ?: PreferenceData().also { INSTANCE = it }
            }

        private const val shpKey = "eu.mcomputing.mobv.zadanie"
        private const val userKey = "userKey"

    }

}

			

2. Použitie:

  • Ukladanie dát: Na ukladanie dát používajte metódu edit() na získanie instancie SharedPreferences.Editor, potom použite metódy ako putInt(), putString(), atď. a nakoniec metódu apply() alebo commit() na uloženie zmien.
  • Načítanie dát: Na načítanie dát používajte metódy ako getInt(), getString(), atď., poskytujte kľúč a predvolenú hodnotu, ktorá sa použije, ak kľúč neexistuje.
class PreferenceData private constructor() {
 ...

    fun clearData(context: Context?) {
        val sharedPref = getSharedPreferences(context) ?: return
        val editor = sharedPref.edit()
        editor.clear()
        editor.apply()
    }

    fun putUser(context: Context?, user: User?) {
        val sharedPref = getSharedPreferences(context) ?: return
        val editor = sharedPref.edit()
        user?.toJson()?.let {
            editor.putString(userKey, it)
        } ?: editor.remove(userKey)

        editor.apply()
    }

    fun getUser(context: Context?): User? {
        val sharedPref = getSharedPreferences(context) ?: return null
        val json = sharedPref.getString(userKey, null) ?: return null

        return User.fromJson(json)
    }
...
}
			
data class User(
    val username: String,
    val email: String,
    val id: String,
    val access: String,
    val refresh: String
)
) {

    fun toJson(): String? {
        return try {
            Gson().toJson(this)
        } catch (ex: IOException) {
            ex.printStackTrace()
            null
        }
    }

    companion object {
        fun fromJson(string: String): User? {
            return try {
                Gson().fromJson(string, User::class.java)
            } catch (ex: IOException) {
                ex.printStackTrace()
                null
            }
        }
    }
}
			

3. Výhody a nevýhody:

  • Výhody: Jednoduché ukladanie malých dát, synchronizácia uložených dát medzi rôznymi časťami aplikácie.
  • Nevýhody: Nie je vhodné na ukladanie veľkých objemov dát alebo citlivých informácií bez šifrovania. Čítanie a písanie prebieha synchronne, čo môže viesť k zablokovaniu hlavného vlákna aplikácie.

4. OAuth autorizacia REST API poziadaviek s ulozenym tokenom

OAuth 2.0 Autorizácia

OAuth 2.0 je široko uznávaný štandard používaný v celom priemysle pre bezpečnú autorizáciu. Umožňuje aplikáciám tretích strán získať obmedzený prístup k HTTP službe buď v mene vlastníka zdroja alebo tým, že aplikácii tretích strán umožní získať prístup na vlastnú päsť.

Ak používateľ autorizuje požiadavku ( prihlasenim sa ), klient dostane autorizačný grant, ktorý je poverením predstavujúcim súhlas používateľa, aby klient mal prístup k jeho zdrojom. Typ poverenia závisí od požadovaného typu grantu (napr. autorizačný kód, implicitný, poverenia hesla vlastníka zdroja alebo poverenia klienta).

Klient požiada o prístupový token z autorizačného servera (koncového bodu) prezentovaním autentifikácie svojej vlastnej identity a autorizačného grantu.

Ak je požiadavka klienta platná, autorizačný server vydá prístupový token klientovi. Klient môže tento token použiť na prístup k zdrojom vlastníka zdroja na zdrojovom serveri.

Prístupový token sa posiela na server zdrojov ako súčasť hlavičky požiadavky. Obvykle sa posiela ako "Bearer" token v hlavičke "Authorization". Server zdrojov potom overí prístupový token a ak je overenie úspešné, poskytne klientovi prístup k požadovaným zdrojom.

Nacitanie profilu

Táto funkcia je asynchrónna úloha, ktorá sa pokúša získať údaje o používateľovi z webovej služby a spracovať rôzne stavy odpovede vrátane obnovenia tokenu.

Funkcia najprv vykonáva požiadavku na získanie údajov o používateľovi. Používa prístupový token a poskytuje ho ako časť "Authorization" hlavičky s predponou "Bearer".

Ak je požiadavka úspešná, funkcia vráti údaje o používateľovi. V opačnom prípade kontroluje, či kód odpovede je 401 (Neautorizovaný), čo znamená, že prístupový token už možno nie je platný.

Pri chybe 401 funkcia pokračuje v pokuse o obnovenie prístupového tokenu. Ak je požiadavka na obnovenie úspešná, funkcia opäť pokúša získať údaje o používateľovi, ale už s novým prístupovým tokenom.

Ak sa vyskytnú chyby počas vykonávania požiadaviek (napr. sieťové chyby, nečakané chyby), funkcia zachytí tieto výnimky, zaznamená ich a vráti chybové správy.

interface ApiService {
	....
	
    @GET("user/get.php")
    suspend fun getUser(
        @HeaderMap header: Map<String, String>,
        @Query("id") id: String
    ): Response<UserResponse>

    @POST("user/refresh.php")
    suspend fun refreshToken(
        @HeaderMap header: Map<String, String>,
        @Body refreshInfo: RefreshTokenRequest
    ): Response<RefreshTokenResponse>
	
}
				
 suspend fun apiGetUser(
        uid: String,
        my_uid: String,
        accessToken: String,
        refreshToken: String
    ): Pair<String, User?> {
        try {
            val response = service.getUser(
                mapOf(
                    "x-apikey" to AppConfig.API_KEY,
                    "Authorization" to "Bearer $accessToken"
                ), uid
            )

            if (response.isSuccessful) {
                response.body()?.let {
                    return Pair(
                        "",
                        User(
                            it.name,
                            "",
                            it.id,
                            accessToken,
                            refreshToken,
                            it.photo
                        )
                    )
                }
            }

            if (response.code() == 401) {
                val refreshResponse = service.refreshToken(
                    mapOf(
                        "x-apikey" to AppConfig.API_KEY,
                        "x-user" to my_uid
                    ), RefreshTokenRequest(refreshToken)
                )
                if (refreshResponse.isSuccessful) {
                    refreshResponse.body()?.let { newtoken ->
                        val response2 = service.getUser(
                            mapOf(
                                "x-apikey" to AppConfig.API_KEY,
                                "Authorization" to "Bearer ${newtoken.access}"
                            ), uid
                        )
                        if (response2.isSuccessful) {
                            response2.body()?.let {
                                return Pair(
                                    "",
                                    User(
                                        it.name,
                                        "",
                                        it.id,
                                        newtoken.access,
                                        newtoken.refresh,
                                        it.photo
                                    )
                                )
                            }
                        }
                    }
                }
            }
            return Pair("Failed to load user", null)
        } catch (ex: IOException) {
            ex.printStackTrace()
            return Pair("Check internet connection. Failed to load user.", null)
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        return Pair("Fatal error. Failed to load user.", null)
    }
				
            

5. Pokročilá autorizácia s Retrofit a OkHttpClient

Integrácia AuthInterceptor a TokenAuthenticator s Retrofit

AuthInterceptor: Interceptor, ktorý sa používa na pridanie autentizačných hlavičiek k požiadavkám pred ich odoslaním. Vytvoríte triedu, ktorá implementuje Interceptor a predefinuje metódu 'intercept'. V tejto metóde pridáte hlavičku 'Authorization' s hodnotou 'Bearer ' + token.

class AuthInterceptor() : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
		// Získajte  token
        val token = ...
		
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(request)
    }
}

TokenAuthenticator: Trieda, ktorá automaticky rieši obnovenie prístupových tokenov. Ak požiadavka vráti HTTP 401, TokenAuthenticator získa nový prístupový token a opätovne vykoná neúspešnú požiadavku.

class TokenAuthenticator() : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        // Získajte nový token
        val newToken = ...

        // Vytvorte novú požiadavku s novým tokenom
        return response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
}

Integrácia s OkHttpClient a Retrofit: Pri vytváraní inštancie Retrofit vytvoríte OkHttpClient, ktorý zahrnie vytvorený AuthInterceptor a TokenAuthenticator. Týmto spôsobom sa zabezpečí, že každá požiadavka bude obsahovať potrebné autentizačné údaje a v prípade vypršania platnosti tokenov budú automaticky obnovené.

val client = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor())
    .authenticator(TokenAuthenticator())
    .build()

val retrofit = Retrofit.Builder()
    .client(client)
    .baseUrl("https://your.api/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Dôsledky: Používanie AuthInterceptor a TokenAuthenticator spolu automatizuje proces autentizácie a obnovy tokenov, čo znižuje riziko neúspešných požiadaviek kvôli problémom s autentizáciou. Avšak, ak sa token neobnoví úspešne, môže to viesť k trvalému zlyhaniu požiadaviek, kým sa používateľ nevráti do aplikácie a neprihlási sa znova.

Integrácia v zadani

Trieda AuthInterceptor je typom interceptora, ktorý pridáva hlavičky k HTTP požiadavkám predtým, ako sú odoslané na server. Tieto hlavičky sú nevyhnutné pre správne fungovanie komunikácie s API, keďže obsahujú informácie o autentizácii a iné dôležité údaje.

Krok 1: Vytvorenie Requestu

Najprv sa vytvorí nový builder požiadavky na základe pôvodnej požiadavky a pridajú sa hlavičky "Accept" a "Content-Type", ktoré definujú, že klient akceptuje JSON formát a že obsah požiadavky je v JSON formáte.

Krok 2: Získanie Tokenu

Token sa získava z preferencií aplikácie pomocou metódy getUser(), ktorá vracia objekt používateľa, z ktorého sa následne získa prístupový token.

Krok 3: Pridanie Hlavičky Autorizácie

Po získaní tokenu sa vytvorí hlavička "Authorization" s hodnotou "Bearer " nasledovanou samotným tokenom. Toto je štandardný spôsob, ako poskytnúť prístupový token v HTTP požiadavkách.

Krok 4: Pridanie API Kľúča

Každá požiadavka potrebuje tiež API kľúč, ktorý je špecifický pre dané API. Tento kľúč sa pridáva do hlavičky s názvom "x-apikey".

Krok 5: Vykonanie Požiadavky

Po pridaniu všetkých hlavičiek sa vykoná požiadavka s novými hlavičkami. Ak server vyžaduje tieto hlavičky pre autentizáciu alebo na spracovanie požiadavky, bez nich by požiadavka neuspela.

class AuthInterceptor(private val context: Context) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
            .newBuilder()
            .addHeader("Accept", "application/json")
            .addHeader("Content-Type", "application/json")

  
         val token = PreferenceData.getInstance().getUser(context)?.access
         request.header("Authorization","Bearer $token")
       
        // add api key to each request
        request.addHeader("x-apikey", AppConfig.API_KEY)

        return chain.proceed(request.build())
    }
}


TokenAuthenticator je trieda v Kotlin, ktorá implementuje Authenticator interface z knižnice OkHttp. Jej úlohou je automatické obnovovanie prístupových tokenov, keď server vráti chybu 401 (neautorizovaný prístup).

Krok 1: Detekcia Chyby 401

Keď metóda authenticate() zistí, že odpoveď serveru má stavový kód 401, znamená to, že aktuálny token je neplatný alebo vypršal.

Krok 2: Získanie Aktuálneho Používateľa a Tokenu

Metóda získa informácie o aktuálnom používateľovi a jeho refresh tokene zo shared preferences aplikácie prostredníctvom triedy PreferenceData.

Krok 3: Obnovenie Tokenu

Pokiaľ je token neplatný, vykoná sa synchronná požiadavka na obnovenie tokenu prostredníctvom metódy refreshTokenBlocking(). Ak je požiadavka úspešná, vytvorí sa nový objekt používateľa s novými tokenmi a uloží sa do shared preferences.

Krok 4: Vytvorenie Novej Požiadavky

Následne sa vytvorí nová požiadavka s novým prístupovým tokenom v hlavičke "Authorization".

Krok 5: Čistenie Dát pri Neúspechu

Ak obnovenie tokenu zlyhá, metóda vyčistí všetky uložené dáta a informácie o používateľovi, aby sa zabránilo neautorizovanému prístupu a ďalším chybám.

Krok 6: Vrátenie Hodnoty

Metóda vráti novú požiadavku s obnoveným tokenom alebo null, ak obnovenie nebolo možné.

class TokenAuthenticator(val context: Context) : Authenticator {
    override fun authenticate(route: Route?, response: okhttp3.Response): Request? {

     
            if (response.code == 401) {
                val userItem = PreferenceData.getInstance().getUser(context)
                userItem?.let { user ->
                    val tokenResponse = ApiService.create(context).refreshTokenBlocking(
                        RefreshTokenRequest(user.refresh)
                    ).execute()

                    if (tokenResponse.isSuccessful) {
                        tokenResponse.body()?.let {
                            val new_user = User(
                                user.username,
                                user.email,
                                user.id,
                                it.access,
                                it.refresh,
                                user.photo
                            )
                            PreferenceData.getInstance().putUser(context, new_user)
                            return response.request.newBuilder()
                                .header("Authorization", "Bearer ${new_user.access}")
                                .build()
                        }
                    }
                }
                //if there was no success of refresh token we logout user and clean any data
                PreferenceData.getInstance().clearData(context)
                return null
            }
        }
        return null
    }
}
interface ApiService {
	....
	
    @GET("user/get.php")
    suspend fun getUser(
        @Query("id") id: String
    ): Response<UserResponse>

    @POST("user/refresh.php")
    suspend fun refreshToken(
        @Body refreshInfo: RefreshTokenRequest
    ): Response<RefreshTokenResponse>
	
    companion object {
        fun create(context: Context): ApiService {

            val client = OkHttpClient.Builder()
                .addInterceptor(AuthInterceptor(context))
                .authenticator(TokenAuthenticator(context))
                .build()

            val retrofit = Retrofit.Builder()
                .baseUrl("https://zadanie.mpage.sk/")
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            return retrofit.create(ApiService::class.java)
        }
    }
}
				

suspend fun apiGetUser(
        uid: String
    ): Pair<String, User?> {
        try {
            val response = service.getUser(uid)

            if (response.isSuccessful) {
                response.body()?.let {
                    return Pair("", User(it.name, "", it.id, "", "", it.photo))
                }
            }

            return Pair("Failed to load user", null)
        } catch (ex: IOException) {
            ex.printStackTrace()
            return Pair("Check internet connection. Failed to load user.", null)
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        return Pair("Fatal error. Failed to load user.", null)
    }

6. Ziskanie povolenia pre zistovanie GPS polohy

Krok 1: Požiadavky na Povolenia

Prvým krokom je požiadanie o povolenia potrebné pre získanie prístupu k presnej polohe zariadenia. V súbore AndroidManifest.xml pridajte nasledujúce riadky:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Krok 2: Integrácia Google Play Služieb

Aby ste mohli používať geofencing, je potrebné integrovať Google Play služby do vášho projektu pridaním nasledujúcej závislosti do vášho build.gradle (Module):

implementation("com.google.android.gms:play-services-location:18.0.0")
Krok 3: Požiadanie o povolenie polohy

Zabezpečte, aby vaša aplikácia požiadala o povolenia v čase behu, ak je to potrebné (Android 6.0 a vyššie).

Pre získanie presnej polohy potrebujeme povolenie ACCESS_FINE_LOCATION. Tu je príklad, ako požiadať o toto povolenie v čase behu:

class ProfileFragment : Fragment() {

    private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)

    val requestPermissionLauncher = registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { isGranted: Boolean ->
            if (isGranted){
                // run logic if user accepted usage of gps location
            }else{
               // run logic if user not accepted usage of gps location
            }
        }

    // returns if Permissions are accepted
    fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
        ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
    }

    ....

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ....

       if (!hasPermissions(requireContext())) {          
          requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
       }
       ....
    }
}   
Pridajte možnosť získavania polohy

Pridajte do profilu alebo nastavení svojej aplikácie, možnosť kde si môže používateľ zapnúť zdieľanie svojej polohy ostatným používateľov v okolí. Pri zapnutí nezabudnite najprv skontrolovať či používateľ povolil prístup k polohe zariadenia, podľa predchádzajúceho kroku. Následne keď používateľ zmení stav získavania, uložte tento stav do SharedPreferences, aby ste tento stav nezabudli pri odchode z obrazovky.

profile location switch
    <androidx.appcompat.widget.SwitchCompat
        android:layout_margin="16dp"
        android:checked="@={model.sharingLocation}"
        android:id="@+id/location_switch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/name"
        app:layout_constraintEnd_toEndOf="parent"
        />
class ProfileViewModel(private val dataRepository: DataRepository) : ViewModel() {
	
	....
	val sharingLocation = MutableLiveData<Boolean?>(null)
	....
}

... vo Fragmente ...

viewModel.sharingLocation.postValue(PreferenceData.getInstance().getSharing(requireContext()))

viewModel.sharingLocation.observe(viewLifecycleOwner){
    it?.let {
        if (it){
            if (!hasPermissions(requireContext())) {
                viewModel.sharingLocation.postValue(false)
                requestPermissionLauncher.launch(
                    Manifest.permission.ACCESS_FINE_LOCATION
                )
            }else{
                PreferenceData.getInstance().putSharing(requireContext(),true)
            }
        }else{
            PreferenceData.getInstance().putSharing(requireContext(),false)
        }
    }
}

7. Zobrazenie GPS polohy na mape

Kontrola Povolení

Kontrola povolení je kľúčová pre zabezpečenie, že aplikácia má správne oprávnenia na prístup k funkciam zariadenia, ako je získavanie aktuálnej polohy. Kód definuje požadované povolenia a metódy na kontrolu týchto povolení.

PERMISSIONS_REQUIRED je pole, ktoré obsahuje všetky povolenia potrebné pre fragment, v tomto prípade ACCESS_FINE_LOCATION.

Metóda hasPermissions prechádza všetky povolenia definované v PERMISSIONS_REQUIRED a kontroluje, či boli udelené. Používa sa funkcia checkSelfPermission pre každé povolenie.

val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)

val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        initLocationComponent()
        addLocationListeners()
    }
}

fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
Inicializácia Mapy a Povolení

V metóde onViewCreated fragment inicializuje mapu a zároveň kontroluje, či má potrebné povolenia. Ak povolenia nie sú udelené, vyžiada si ich od používateľa pomocou requestPermissionLauncher. Ak sú povolenia udelené, pokračuje v inicializácii komponentov pre prácu s polohou a nastaví poslucháčov pre aktualizácie polohy.

requestPermissionLauncher je inštancia, ktorá sa stará o výsledok požiadavky na povolenie. V prípade, že je povolenie udelené, volajú sa metódy initLocationComponent a addLocationListeners.


annotationManager = bnd.mapView.annotations.createCircleAnnotationManager()

val hasPermission = hasPermissions(requireContext())
onMapReady(hasPermission)

bnd.myLocation.setOnClickListener {
    if (!hasPermissions(requireContext())) {
        requestPermissionLauncher.launch(
            Manifest.permission.ACCESS_FINE_LOCATION
        )
    } else {
        lastLocation?.let { refreshLocation(it) }
        addLocationListeners()
        Log.d("MapFragment","location click")
    }
}

Manipulácia s Mapou

Metóda onMapReady sa zavolá, keď je mapa pripravená. Nastaví kameru mapy, načíta štýl a pridá poslucháčov pre kliknutie na mapu a zmeny polohy. Ak je povolenie udelené, inicializuje sa komponent polohy:

private fun onMapReady(enabled: Boolean) {
        binding.mapView.getMapboxMap().setCamera(
            CameraOptions.Builder()
                .center(Point.fromLngLat(14.3539484, 49.8001304))
                .zoom(2.0)
                .build()
        )
        binding.mapView.getMapboxMap().loadStyleUri(
            Style.MAPBOX_STREETS
        ) {
            if (enabled) {
                initLocationComponent()
                addLocationListeners()
            }
        }

        binding.mapView.getMapboxMap().addOnMapClickListener {
            if (hasPermissions(requireContext())) {
                onCameraTrackingDismissed()
            }
            true
        }
    }
}

Metóda addMarker pridáva značku na mapu na základe poskytnutej polohy.

Aktualizácia a Sledovanie Polohy

Metódy initLocationComponent a addLocationListeners sa starajú o inicializáciu komponentov polohy a pridanie poslucháčov pre zmeny polohy.

initLocationComponent je metóda, ktorá inicializuje komponent pre získavanie aktuálnej polohy používateľa. Nastavenia komponentu umožňujú jeho aktiváciu a zapnutie vizuálneho efektu pulzovania pre aktuálnu polohu.

addLocationListeners pridáva poslucháčov, ktoré sledujú zmeny polohy a gestá na mape. Tieto poslucháče zahŕňajú OnIndicatorPositionChangedListener pre zmenu polohy a OnMoveListener pre sledovanie gest pohybu.

Pri zmene polohy, metóda refreshLocation aktualizuje kameru mapy, aby zobrazovala novú polohu a pridá marker na túto polohu.


    private fun initLocationComponent() {
        Log.d("MapFragment","initLocationComponent")
        val locationComponentPlugin = binding.mapView.location
        locationComponentPlugin.updateSettings {
            this.enabled = true
            this.pulsingEnabled = true
        }

    }

    private fun addLocationListeners() {
        Log.d("MapFragment","addLocationListeners")
        binding.mapView.location.addOnIndicatorPositionChangedListener(
                onIndicatorPositionChangedListener
        )
        binding.mapView.gestures.addOnMoveListener(onMoveListener)

    }

onIndicatorPositionChangedListener sa aktivuje, keď sa zmení poloha indikátora polohy (zvyčajne poloha používateľa). Pri zmene polohy zaznamenáva novú polohu a volá funkciu refreshLocation() s novou polohou.

refreshLocation(it) aktualizuje užívateľské rozhranie mapy tak, aby odrážalo novú polohu.

refreshLocation(point: Point) funkcia sa volá na aktualizáciu užívateľského rozhrania mapy na novú polohu používateľa. Vykonáva niekoľko akcií:

  • Centruje kameru mapy na novú polohu: binding.mapView.getMapboxMap().setCamera(CameraOptions.Builder().center(point).zoom(14.0).build())
  • Nastavuje stredový bod mapy na novú polohu pre interakcie gestami: binding.mapView.gestures.focalPoint = binding.mapView.getMapboxMap().pixelForCoordinate(point)
  • Aktualizuje lastLocation na aktuálnu polohu.
  • Pridáva značku na aktuálnu polohu s funkciou addMarker(point).

onMoveListener reaguje na pohybové udalosti na mape. Má tri metódy:

  • onMoveBegin(detector: MoveGestureDetector): Volá sa na začiatku pohybu. Aktivuje metódu onCameraTrackingDismissed().
  • onMove(detector: MoveGestureDetector): Boolean: Reaguje na udalosti pohybu. V tomto prípade nevykonáva žiadnu akciu a vráti false.
  • onMoveEnd(detector: MoveGestureDetector): Volá sa na konci pohybu. V tomto prípade nevykonáva žiadnu akciu.

onCameraTrackingDismissed() sa volá, keď je potrebné zrušiť sledovanie polohy kamery vzhľadom na polohu používateľa. Odstraňuje onIndicatorPositionChangedListener a onMoveListener z príslušných komponentov.

    private val onIndicatorPositionChangedListener = OnIndicatorPositionChangedListener {
        Log.d("MapFragment","poloha je $it")
        refreshLocation(it)
    }

    private fun refreshLocation(point: Point) {
        binding.mapView.getMapboxMap().setCamera(CameraOptions.Builder().center(point).zoom(14.0).build())
        binding.mapView.gestures.focalPoint = binding.mapView.getMapboxMap().pixelForCoordinate(point)
        lastLocation = point
        addMarker(point)

    }
	
    private val onMoveListener = object : OnMoveListener {
        override fun onMoveBegin(detector: MoveGestureDetector) {
            onCameraTrackingDismissed()
        }

        override fun onMove(detector: MoveGestureDetector): Boolean {
            return false
        }

        override fun onMoveEnd(detector: MoveGestureDetector) {}
    }


    private fun onCameraTrackingDismissed() {
        binding.mapView.apply {
            location.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener)
            gestures.removeOnMoveListener(onMoveListener)
        }
    }

Tieto metódy zabezpečujú, že ak používateľ posunie mapu alebo sa jeho poloha zmení, mapa sa aktualizuje a zobrazí správne informácie.

Upratanie listenerov pri ukonceni/minimalizovani aplikacie

Metóda onDestroyView() je súčasťou životného cyklu fragmentu v Android aplikáciách. Táto metóda sa automaticky volá, keď je pohľad fragmentu odstraňovaný a je na ceste k zničeniu. Je to miesto, kde môžete uvoľniť všetky zdroje, ktoré nie sú viac potrebné a ktoré ste pravdepodobne inicializovali v metóde onCreateView() alebo onViewCreated().

  • location.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener): Odstraňuje počúvač, ktorý bol zaregistrovaný na sledovanie zmien polohy indikátora.
  • gestures.removeOnMoveListener(onMoveListener): Odstraňuje počúvač, ktorý bol zaregistrovaný na sledovanie pohybových udalostí na mape.

Týmto spôsobom metóda onDestroyView() pomáha predchádzať úniku pamäte odstraňovaním počúvačov, keď už nie sú potrebné, čo je dôležitou súčasťou správy zdrojov v Android aplikáciách.

Zobrazenie polohy vysledok
location screen

Preštudujte a odskúšajte si kódy hlavne z týchto stránok: