Cvičenie 9,10 - Geofence, WorkManager, Notifikacie
Ú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
}
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)
}
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()
Ď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()
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.
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)
}
}
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.