Úlohy pre 9. a 10. cvičenie

  • 0. Nahranie/odstranenie profilovej fotky
  • 1. Ziskanie polohy a vytvorenie Geofence okolo nej
  • 2. Workmanager
  • 3. Notifikácie
  • 4. Testovanie UI a UX
  • 5. Testovanie aplikácie
  • 6. Vylepšenie UI a UX

0. Nahranie/odstranenie profilovej fotky

Vybratie fotky na zariadeni

Aby si vedel pouzivatel vybrat fotku, zo svojho zariadenia potrebujete si vypytat od neho povolenia na pristup ku fotkam co ma v telefone.

Zalezi od verzie Android ale vo vseobecnosti :

    
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

Ukazkovy kod

Cely ukazkovy kod, ako otvorit okno na vyber fotiek a konverziu uri na obrazok najde na stiahnutie tu : PhotoFragment.kt. Upozornujeme, ze treba v nom poziadat o povolenia a osetrit, ked vam niektore povolenie nahodou zamietne. Nezabudnite dat povolenia aj do AndroidManifest.xml

Otvorenie dialogu

lifecycleScope.launch {
	val mimeType = "image/jpeg"
	pickMedia.launch(
		PickVisualMediaRequest(
			ActivityResultContracts.PickVisualMedia.SingleMimeType(
				mimeType
			)
		)
	)
}

Konverzia Uri na Subor:

 fun inputStreamToFile(
        uri: Uri,
    ): File? {
        val resolver = requireContext().applicationContext.contentResolver
        resolver.openInputStream(uri).use { inputStream ->
            var orig = File(requireContext().filesDir, "photo_copied.jpg")
            if (orig.exists()) {
                orig.delete()
            }
            orig = File(requireContext().filesDir, "photo_copied.jpg")

            FileOutputStream(orig).use { fileOutputStream ->
                if (inputStream == null) {
                    Log.d("vybrane", "stream null")
                    return null
                }
                try {
                    Log.d("vybrane", "copied")
                    inputStream.copyTo(fileOutputStream)
                } catch (e: IOException) {
                    e.printStackTrace()
                    return null
                }
            }
            Log.d("vybrane", orig.absolutePath)
            return orig
        }

    }

REST API

Do zoznamu REST API pribudli dve nove akcie na nahratie fotky a odstranenie fotky. Pouzite ich na to aby pouzivatel vedel nahrat svoju fotku, pripadne vymazat fotku ktoru nahral predtym.



### Nahratie profilovej fotky -  !!!!!!! POZOR - upload.mcomputing.eu  a nie zadanie.mpage.sk ako pri inych requestoch !! 
### odosielate fotku JPG alebo JPEG ako form data, kde je jedna hodnota "image" s profilovou fotkou.

POST https://upload.mcomputing.eu/user/photo.php
Authorization: Bearer ...
x-apikey: ...
Content-Type: multipart/form-data; boundary=WebAppBoundary

--WebAppBoundary
Content-Disposition: form-data; name="image"; filename="photo.jpg"
Content-Type: image/jpg

< ./mojafotka.jpg
--WebAppBoundary--

### Ocakavana odpoved

{"id":5,"name":"a","photo":"photos/u-5.jpeg"}

### Odstranenie profilovej fotky 

DELETE https://upload.mcomputing.eu/user/photo.php
Authorization: Bearer ...
x-apikey: ...

### Ocakavana odpoved

{"id":5,"name":"a","photo":""}

import okhttp3.MultipartBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface UploadService {
    @Multipart
    @POST("https://upload.mcomputing.eu/photo.php")
    suspend fun uploadImage(
        @Part image: MultipartBody.Part
    ): Response
}


import android.content.Context
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File

suspend fun uploadImage(imageUri: Uri) {

    val file = File(imageUri.path ?: "")
    val requestFile = RequestBody.create("image/jpg".toMediaTypeOrNull(), file)
    
    val body = MultipartBody.Part.createFormData("image", file.name, requestFile)
    
    // Call the uploadImage method
    val call = VasaRetrofitObjekt.uploadImage(body)
   
}

1. Ziskanie polohy a vytvorenie Geofence okolo nej

Povolenie prístupu k lokalizácii na pozadí

Povolenie <uses-permission-sdk-23 android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> v súbore AndroidManifest.xml je nevyhnutné pre aplikácie, ktoré potrebujú získavať prístup k lokalizácii zariadenia, keď aplikácia beží na pozadí alebo nie je aktívna na obrazovke. Toto povolenie je špecifikované pre aplikácie cieľujúce na Android SDK verziu 23 a vyššiu. Od tejto verzie Androidu sú pravidlá pre prístup k lokalizácii na pozadí prísnejšie kvôli ochrane súkromia užívateľa. Aplikácie, ktoré chcú využívať túto schopnosť, by mali byť transparentné voči užívateľovi a jasne komunikovať, prečo potrebujú takýto prístup, aby mohli získať potrebné povolenie od užívateľa.

Nižšie uvádzam ukážkový kód ako je možné overiť či použivateľ udelil dostatočné povolenia, a tiež ako ich vyžiadať ak ich ešte nepovolil.


private val PERMISSIONS_REQUIRED = when {
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_BACKGROUND_LOCATION
        )
    }

    else -> {
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
    }
}

val requestPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {

    }

fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}

Krok 1: Získanie Aktuálnej Polohy

Pred vytvorením geofence potrebujete získať aktuálnu polohu zariadenia. To môžete urobiť použitím FusedLocationProviderClient. Nezabudnite skontrolovať, či máte povolenie na prístup k polohe pred tým, ako získate polohu.


val fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
fusedLocationClient.lastLocation.addOnSuccessListener(requireActivity()) {
    // Logika pre prácu s poslednou polohou
    Log.d("ProfileFragment", "poloha posledna $it")
    setupGeofence(it)
}

Krok 2: Vytvorenie Geofence

Po získaní aktuálnej polohy môžete nastaviť geofence. Tu je príklad, ako vytvoriť geofence s určitým polomerom okolo aktuálnej polohy:


val geofencingClient = LocationServices.getGeofencingClient(requireActivity())

val geofence = Geofence.Builder()
    .setRequestId("my-geofence")
    .setCircularRegion(location.latitude, location.longitude, 100f) // 100m polomer
    .setExpirationDuration(Geofence.NEVER_EXPIRE)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_EXIT)
    .build()

Krok 3: Požiadavka na Geofencing

Ďalej potrebujete vytvoriť požiadavku na geofencing a pridať geofence. Geofencing je technológia, ktorá umožňuje aplikácii reagovať na vstup alebo výstup z definovanej geografickej oblasti, čo sa často nazýva "virtuálny plot".

Vytvorenie objektu GeofencingRequest: Prostredníctvom konštruktora Builder() sa vytvára nový objekt GeofencingRequest. Táto konštrukcia umožňuje nastaviť rôzne parametre a vlastnosti pre požiadavku na geofencing.

Nastavenie počiatočneho stavu: Metóda setInitialTrigger() definuje, aká akcia spustí upozornenie geofencingu. V tomto prípade je nastavená na INITIAL_TRIGGER_ENTER, čo znamená, že upozornenie bude spustené hneď ako užívateľ vstúpi do definovanej oblasti.

Pridanie geofence: Metóda addGeofence() pridáva konkrétny geofence (virtuálny plot) do požiadavky. V tomto prípade je geofence predom definovaný objekt, ktorý určuje geografické hranice a pravidlá pre geofencing.

Nakoniec metóda build() vytvára a vráti objekt GeofencingRequest s nastavenými vlastnosťami.

Výsledná požiadavka na geofencing môže byť následne použitá s rôznymi službami, ktoré monitorujú polohu zariadenia a vyvolávajú upozornenia alebo akcie, keď sú splnené podmienky geofencingu.


val geofencingRequest = GeofencingRequest.Builder()
    .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
    .addGeofence(geofence)
    .build()

Krok 4: Geofence PendingIntent

Na zachytenie udalostí geofence potrebujete PendingIntent, ktorý spustí službu alebo vysielanie, keď sa vykoná udalosť geofence:


val intent = Intent(requireActivity(), GeofenceBroadcastReceiver::class.java)
val geofencePendingIntent =
    PendingIntent.getBroadcast(
        requireActivity(),
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT
    )

V danom kóde sa vytvára Intent, ktorý definuje, že chceme spustiť službu GeofenceBroadcastReceiver. Tento Intent je potom zabalený do PendingIntent, čo umožňuje externým aplikáciám alebo službám spustiť tento Intent v našom mene. Použitím PendingIntent.FLAG_UPDATE_CURRENT zabezpečujeme, že ak už existuje takýto PendingIntent, bude aktualizovaný novými dátami z aktuálneho Intentu.

Upozornenie: GeofenceBroadcastReceiver nebude Android Studio poznať, keďže ste ho ešte neimplementovali, jeho implementácia je nižšie.

Krok 5: Pridanie Geofences k LocationServices.getGeofencingClient

Nakoniec, pridajte váš geofence pomocou LocationServices.getGeofencingClient:


geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run {
    addOnSuccessListener {
        // Geofences boli úspešne pridané
        Log.d("ProfileFragment", "geofence vytvoreny")
    }
    addOnFailureListener {
        // Chyba pri pridaní geofences
        it.printStackTrace()
        binding.locationSwitch.isChecked = false
        PreferenceData.getInstance().putSharing(requireContext(), false)
    }
}

Spracovanie Geofence na pozadi

Ak chcete reagovať na prechody geofence, vytvorte BroadcastReceiver, ktorý zachytí udalosti geofence. Počas udalosti EXIT môžete získať novú polohu na pozadí:

class GeofenceBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent == null) {
            // no geofence exit
            Log.e("GeofenceBroadcastReceiver", "error 1")
            return
        }

        val geofencingEvent = GeofencingEvent.fromIntent(intent)

        if (geofencingEvent == null) {
            // error
            Log.e("GeofenceBroadcastReceiver", "error 2")
            return
        }

        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes
                .getStatusCodeString(geofencingEvent.errorCode)
            //send error message to user using notification
            Log.e("GeofenceBroadcastReceiver", "error 3")
            return
        }

        // Get the transition type.
        val geofenceTransition = geofencingEvent.geofenceTransition

        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
            val triggeringLocation = geofencingEvent.triggeringLocation
            if (context == null || triggeringLocation == null) {
                // error
                Log.e("GeofenceBroadcastReceiver", "error 4")
                return
            }
            setupGeofence(triggeringLocation, context)
        }
    }

    private fun setupGeofence(location: Location, context: Context) {

        val geofencingClient = LocationServices.getGeofencingClient(context.applicationContext)

        val geofence = Geofence.Builder()
            .setRequestId("my-geofence")
            .setCircularRegion(location.latitude, location.longitude, 100f) // 100m polomer
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_EXIT)
            .build()

        val geofencingRequest = GeofencingRequest.Builder()
            .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
            .addGeofence(geofence)
            .build()

        val intent = Intent(context, GeofenceBroadcastReceiver::class.java)
        val geofencePendingIntent =
            PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // permission issue, geofence not created
            Log.e("GeofenceBroadcastReceiver", "error 5")
            return
        }
        geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run {
            addOnSuccessListener {
                // Geofences boli úspešne pridané
                Log.d("GeofenceBroadcastReceiver", "novy geofence vytvoreny")
            }
            addOnFailureListener {
                // Chyba pri pridaní geofences
                Log.e("GeofenceBroadcastReceiver", "error 6")
            }
        }

    }
}

2. Workmanager

1. Úvod do WorkManagera

WorkManager je knižnica určená na plánovanie odložiteľných úloh na pozadí, zabezpečuje vykonanie úloh aj keď je aplikácia zatvorená alebo zariadenie reštartované. Je vhodný pre úlohy ako nahrávanie logov, aplikovanie filtrov na obrázky alebo pravidelná synchronizácia lokálnych dát so sieťou.

2. Pridanie WorkManagera do vášho projektu

Pridajte závislosť WorkManager do vášho súboru app/build.gradle:


dependencies {
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}
                
Synchronizujte svoj projekt, aby sa zabezpečilo, že závislosť je správne načítaná.

3. Vytvorenie vašej prvej práce

Definujte triedu Worker, aby ste určili prácu, ktorú chcete vykonať:


import androidx.work.Worker
import androidx.work.WorkerParameters

class MyWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        // Tu môžete vykonávať synchrónnu prácu

        return Result.success()
    }
}
                

Alebo pre asynchrónne úlohy (suspend):


import androidx.work.Worker
import androidx.work.WorkerParameters

class MyWorker(appContext: Context, workerParams: WorkerParameters)
    : CoroutineWorker(appContext, workerParams) {
    
    override suspend fun doWork(): Result {
        // Tu môžete vykonávať asynchrónnu prácu

        return Result.success()
    }
}
                
Vytvorte WorkRequest, aby ste nakonfigurovali, ako a kedy spustiť vašu prácu jednorazovo:

val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>().build()
                
Zaradte vašu prácu do WorkManagera:

WorkManager.getInstance(myContext).enqueue(myWorkRequest)
                

Zdroj obrazka: https://developer.android.com/guide/background/persistent/how-to/states

Alternatívne vytvorte WorkRequest, aby ste spustili vašu prácu opakovane:


val repeatingRequest = PeriodicWorkRequestBuilder<MyWorker>(
1, TimeUnit.HOURS, // repeatInterval
15, TimeUnit.MINUTES // flexInterval
).build()
                
Zaradte vašu prácu do WorkManagera:

WorkManager.getInstance(myContext).enqueueUniquePeriodicWork(
"Unique Name",
ExistingPeriodicWorkPolicy.KEEP, // or REPLACE
repeatingRequest
)
                

Zdroj obrazka: https://developer.android.com/guide/background/persistent/how-to/states

4. Podmienky spustenia

Nastavte obmedzenia, aby ste určili podmienky, za ktorých by sa mala vaša práca spustiť:


val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setConstraints(constraints)
    .build()
                

Nastavenie oneskoreného spustenia.

				 
val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
   .setInitialDelay(10, TimeUnit.MINUTES)
   .build()
   

Nastavenie politiky opakovania sa neúspešnej úlohy ( doWork() musí vrátiť Result.retry() )

	
 val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

5. Vstupne a vystupne data

Definovanie vstupných dát:


val inputData = workDataOf("key1" to "value1", "key2" to 2)

val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setInputData(inputData)
    .build()
                

Prístup k vstupným dátam vo vašom Workeri:


class MyWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        val inputData = inputData
        val value1 = inputData.getString("key1")
        val value2 = inputData.getInt("key2", 0)

        // Do the work here

        return Result.success()
    }
}
                

Definovanie výstupných dát:


class MyWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        // Do the work here

        val outputData = workDataOf("outputKey" to "outputValue")

        return Result.success(outputData)
    }
}
                

6. Odovzdavanie parametrov

Prístup k výstupným dátam v nasledujúcom Worker-i:


class FirstWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        // Do the work here
        val inputData = inputData
        val value1 = inputData.getInt("key1", 0)
        val value2 = inputData.getInt("key2", 0)
		
        val outputData = workDataOf("outputKey" to value1+value2)

        return Result.success(outputData)
    }
}

class SecondWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        val inputData = inputData
        val value1 = inputData.getString("outputKey")

        // Do the work here

        return Result.success()
    }
}

val inputData = workDataOf("key1" to 3, "key2" to 2)

val firstRequest = OneTimeWorkRequestBuilder<FirstWorker>().setInputData(inputData).build()
val secondRequest = OneTimeWorkRequestBuilder<SecondWorker>().setInputMerger(OverwritingInputMerger::class).build()

WorkManager.getInstance(myContext)
    .beginWith(firstRequest)
    .then(secondRequest)
    .enqueue()
                

7. Možné návratové stavy z metódy doWork()

Metóda doWork() v triede Worker môže vrátiť tri rôzne stavy, ktoré oznámia WorkManageru výsledok vykonávania práce. Tieto stavy sú:

1. Result.success():


override fun doWork(): Result {
    // ... your code ...

    return Result.success() // alebo Result.success(data)
}
                
Tento stav označuje, že práca bola úspešne dokončená.

2. Result.failure():


override fun doWork(): Result {
    // ... your code ...

    return Result.failure() // alebo Result.failure(data)
}
                
Tento stav označuje, že práca zlyhala a nemala by sa pokúsiť znova.

3. Result.retry():


override fun doWork(): Result {
    // ... your code ...

    return Result.retry()  
}
                
Tento stav označuje, že práca zlyhala, ale mala by sa pokúsiť znova neskôr.

8. Úprava existujúceho WorkRequestu

Úprava existujúceho WorkRequestu (používa sa len vo špeciálnych prípadoch) :

suspend fun updatePhotoUploadWork() {
    // Get instance of WorkManager.
    val workManager = WorkManager.getInstance(context)

    // Retrieve the work request ID. In this example, the work being updated is unique
    // work so we can retrieve the ID using the unique work name.
    val photoUploadWorkInfoList = workManager.getWorkInfosForUniqueWork(
        PHOTO_UPLOAD_WORK_NAME
    ).await()

    val existingWorkRequestId = photoUploadWorkInfoList.firstOrNull()?.id ?: return

    // Update the constraints of the WorkRequest to not require a charging device.
    val newConstraints = Constraints.Builder()
        // Add other constraints as required here.
        .setRequiresCharging(false)
        .build()

    // Create new WorkRequest from existing Worker, new constraints, and the id of the old WorkRequest.
    val updatedWorkRequest: WorkRequest =
        OneTimeWorkRequestBuilder<MyWorker>()
            .setConstraints(newConstraints)
            .setId(existingWorkRequestId)
            .build()

    // Pass the new WorkRequest to updateWork().
    workManager.updateWork(updatedWorkRequest)
}

9. Monitorovanie a ladenie

Použite logcat v Android Studio na sledovanie logov WorkManagera.

3. Notifikácie

1. Povolenia pre zobrazenie oznámenia

Od verzie Android 13 je potrebné získať povolenie od používateľa pred tým, ako mu môžete zaslať oznámenia. Tento model pomáha znížiť prerušenia oznámení, minimalizovať preťaženie informáciami a pomáha používateľom kontrolovať, ktoré oznámenia sa zobrazia na základe toho, čo je pre nich dôležité.

Pre získanie povolenia na zobrazenie oznámení je potrebné pridať nasledujúci riadok do vášho AndroidManifest.xml súboru:


<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
            

Toto povolenie je nevyhnutné, aby vaša aplikácia mohla zobrazovať oznámenia používateľom. Následne pre Android 13 musíte vyžiadať povolenie na zobrazenie notifikácií od používateľa rovnako ako pri GPS polohe. Najlepšie to urobiť hneď spolu so žiadaním povolenia o GPS polohu.

2. Vytvorenie Notification Channel (Kanála oznámení)

Pred zobrazením oznámenia je potrebné vytvoriť Notification Channel.

// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val name = "MOBV Zadanie"
    val descriptionText = "Popis notifikacie"
    val importance = NotificationManager.IMPORTANCE_DEFAULT
	val channel_id = "kanal-1"
    val channel =
        NotificationChannel(channel_id, name, importance).apply {
            description = descriptionText
        }
    // Register the channel with the system
    val notificationManager: NotificationManager =
        appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    notificationManager.createNotificationChannel(channel)
}

3. Zobrazenie oznámenia

Aby ste mohli zobraziť oznámenia z Worker-a, je potrebné najprv vytvoriť základné oznámenie pomocou objektu NotificationCompat.Builder. Toto oznámenie môže zobrazovať ikonu, názov a malé množstvo textového obsahu, ktoré môže používateľ ťuknúť na spustenie aktivity vo vašej aplikácii. Oznámenia sú správy, ktoré Android zobrazuje mimo rozhrania vašej aplikácie, aby poskytol používateľovi pripomienky, komunikáciu od iných osôb alebo iné aktuálne informácie z vašej aplikácie.

Tu je príklad kódu v jazyku Kotlin, ktorý demonštruje, ako vytvoriť a zobraziť oznámenie:

val name = "MOBV Zadanie"
val descriptionText = "Popis notifikacie"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel_id = "kanal-1"
val builder =
    NotificationCompat.Builder(appContext, channel_id).apply {
        setContentTitle("Titulok")
        setContentText("Text notifikacie")
        setSmallIcon(R.drawable.ikona_notifikacie)
        priority = NotificationCompat.PRIORITY_DEFAULT
    }

if (ActivityCompat.checkSelfPermission(
        appContext,
        Manifest.permission.POST_NOTIFICATIONS
    ) != PackageManager.PERMISSION_GRANTED
) {
    Log.d("Notifikacia","Chyba povolenie na notifikaciu");
    return
}

NotificationManagerCompat.from(context).notify(1, builder.build())

Vytvorenie kanála a zobrazenie notifikácie môže urobiť vo vnútri Workera (alebo Fragmentu či BroadcastReceivera).

4. Testovanie UI a UX

Stiahnite a nainstalujte si aplikáciu z Google Play: Kontrola dostupnosti. Nasledne ju pouzite na vyhodnotenie UI vasej aplikacie.

5. Testovanie aplikácie

Skusajte vasu aplikaciu ci vam vsetko funguje. Skuste vsetko co sa da vo vasej aplikacii. Rovnako skuste aj vypnut povolenie polohy, notifikacii, vypnut internet (mobilny aj WiFi). Testovat aplikaciu v roznych situaciach.

Nasledne skuste dat vasu aplikaciu otestovat vasemu kamaratovi alebo spolubyvajucemu pripadne studentovi, ktory sedi vedla vas na cviceni. Pozerajte ako pouziva vasu aplikaciu a zapisujte si, kedy podla vas zbytocne klikal alebo sa zasekol v niektorom kroku a chvilu mu trvalo kym sa dostal tam kde chcel. Tiez si zapisujte ked mu aplikacia spadla (pri akej cinnosti), pripadne ine neocakavane spravanie aplikacie pripadne pouzivatela.

6. Vylepšenie UI a UX.

Zistene nedostatky zistene z aplikacie "Služby dostupnosti pre Android" a tiez chyby, ktore ste zistili pri testovani aplikacie opravte v aplikacii.