From 9c3994e05312415f8a8d73bb191227b7f245b0b4 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 8 Nov 2023 09:21:33 +0600 Subject: [PATCH 1/2] 7330-On_crash_of_SQLiteFullException_show_notification issue: https://gitlab.e.foundation/e/backlog/-/issues/7330 App can crash because of low storage, & it throws SQLiteFullException then. If that occurs, we want to let user know why app crashed. This implementation also enables us to log unhandled exception in the logcat. --- .../foundation/e/apps/AppLoungeApplication.kt | 6 ++ .../java/foundation/e/apps/MainActivity.kt | 2 +- .../install/workmanager/InstallAppWorker.kt | 24 +----- .../utils/AppsUncaughtExceptionHandler.kt | 77 +++++++++++++++++++ .../e/apps/utils/NotificationUtils.kt | 55 +++++++++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-it/strings.xml | 2 + app/src/main/res/values/strings.xml | 4 + 10 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt create mode 100644 app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt diff --git a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt index 368c89a3c..b8156fedb 100644 --- a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +++ b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt @@ -34,6 +34,7 @@ import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.install.workmanager.InstallWorkManager import foundation.e.apps.ui.setup.tos.TOS_VERSION +import foundation.e.apps.utils.AppsUncaughtExceptionHandler import foundation.e.lib.telemetry.Telemetry import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.MainScope @@ -59,10 +60,15 @@ class AppLoungeApplication : Application(), Configuration.Provider { @Inject lateinit var preferenceManagerModule: PreferenceManagerModule + @Inject + lateinit var uncaughtExceptionHandler: AppsUncaughtExceptionHandler + @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate() { super.onCreate() + Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler) + InstallWorkManager.context = this // Register broadcast receiver for package manager val pkgManagerBR = object : PkgManagerBR() {} diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 5b94937da..a2715a1e8 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -40,8 +40,8 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.login.LoginViewModel +import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.databinding.ActivityMainBinding diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt index e26c1df66..b31d74cb6 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt @@ -18,11 +18,7 @@ package foundation.e.apps.install.workmanager -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context -import android.os.Build -import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo @@ -31,6 +27,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R +import foundation.e.apps.utils.NotificationUtils import java.util.concurrent.atomic.AtomicInteger @HiltWorker @@ -74,26 +71,13 @@ class InstallAppWorker @AssistedInject constructor( val cancel = applicationContext.getString(R.string.cancel) // This PendingIntent can be used to cancel the worker val intent = WorkManager.getInstance(applicationContext) - .createCancelPendingIntent(getId()) + .createCancelPendingIntent(id) - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as - NotificationManager - // Create a Notification channel if necessary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val mChannel = NotificationChannel( - "applounge_notification", - title, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(mChannel) - } + NotificationUtils.createBasicNotificationChannel(context) - val notification = NotificationCompat.Builder(applicationContext, "applounge_notification") - .setContentTitle(title) + val notification = NotificationUtils.getBasicNotificationBuilder(context) .setTicker(title) .setContentText(progress) - .setSmallIcon(R.drawable.app_lounge_notification_icon) .setOngoing(true) // Add the cancel action to the notification which can // be used to cancel the worker diff --git a/app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt b/app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt new file mode 100644 index 000000000..25c4980a7 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt @@ -0,0 +1,77 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.utils + +import android.Manifest +import android.app.Notification +import android.content.Context +import android.content.pm.PackageManager +import android.database.sqlite.SQLiteFullException +import androidx.annotation.StringRes +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AppsUncaughtExceptionHandler @Inject constructor( + @ApplicationContext private val context: Context +) : Thread.UncaughtExceptionHandler { + + companion object { + private const val NOTIFICATION_ID = 404 + } + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + Timber.e(throwable, "unhandled exception is caught at thread: ${thread.name}") + + if (throwable is SQLiteFullException || throwable.cause is SQLiteFullException) { + showNotification(R.string.notification_content_full_db) + } + } + + private fun showNotification(@StringRes contentId: Int) { + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + + NotificationUtils.createBasicNotificationChannel(context) + notify(NOTIFICATION_ID, getNotification(contentId)) + } + } + + private fun getNotification(@StringRes contentId: Int): Notification { + val content = context.getString(contentId) + + return NotificationUtils.getBasicNotificationBuilder(context) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .setContentText(content) + .build() + } +} diff --git a/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt b/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt new file mode 100644 index 000000000..b9c8ff1a8 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt @@ -0,0 +1,55 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import foundation.e.apps.R + +object NotificationUtils { + + fun createBasicNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val id = context.getString(R.string.generic_notification_channel_id) + val title = context.getString(R.string.app_name) + + val channel = NotificationChannel( + id, + title, + NotificationManager.IMPORTANCE_LOW + ) + + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + fun getBasicNotificationBuilder(context: Context): NotificationCompat.Builder { + val channelId = context.getString(R.string.generic_notification_channel_id) + val title = context.getString(R.string.app_name) + + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.app_lounge_notification_icon) + .setContentTitle(title) + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aafa2cb17..4f17154f8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -169,4 +169,6 @@ Aktualisierungen werden überprüft … Das anonyme Konto, das von Ihnen genutzt wird, ist nicht verfügbar. Bitte erneuern (refresh) Sie die Sitzung, um ein neues anonymes Konto zu erhalten. SITZUNG ERNEUERN + + Bitte etwas Platz auf dem Telefon freimachen, damit die App Lounge ordnungsgemäß funktionieren kann. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ac0af2df9..6ed760d13 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -155,4 +155,6 @@ PWA y aplicaciones de código abierto O mostrar sólo Libera %1$s en tu teléfono para recibir las últimas actualizaciones. + + Por favor, libera algo de espacio en tu teléfono para que App Lounge funcione correctamente. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7ec04744c..fcb3a6bab 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -166,4 +166,6 @@ \n\t• limiter le micro-ciblage \n\t• limiter les impacts au cas où ce compte serait restreint par Google Libérez %1$s sur votre téléphone afin de bénéficier des dernières mises à jour. + + Merci de libérer de l\'espace sur votre téléphone pour qu\'App Lounge puisse fonctionner correctement. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 537c56f53..21908d07f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -166,4 +166,6 @@ \n\t• ridurre il micro-targeting \n\t• limitare l\'impatto nel caso in cui l\'account venga bloccato da Google Per scaricare l\'aggiornamento, devi liberare %1$s di spazio sullo smartphone. + + Ti invitiamo a liberare un pò di spazio sul telefono in modo che App Lounge possa funzionare correttamente. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c404c57a8..053c733af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,8 @@ App Lounge + applounge_notification + Home Categories @@ -234,4 +236,6 @@ Clicking on \"%1$s\" will open a tab in your browser with the app\'s package name prefilled.<br /><br />Click on \"Perform analysis\" to start the analysis by Exodus.<br /><br />When the button \"See the report\" is displayed (it can take a while depending on the app) you can close the tab and go back to the app description in %2$s where you should see the Privacy Score. Sometimes Exodus can fail to analyze the app.<br /><br />NB: it can take up to 10 min for the score to be displayed in the app description. Free up %1$s on your phone to get the latest updates. + + Please free up some space on your phone so App Lounge can work properly. -- GitLab From 53834593d34087df89beb6a468a5e3c71282632b Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Fri, 10 Nov 2023 10:36:29 +0600 Subject: [PATCH 2/2] refactor according to review --- .../foundation/e/apps/AppLoungeApplication.kt | 4 +- .../install/workmanager/InstallAppWorker.kt | 24 +++++++- ...r.kt => CustomUncaughtExceptionHandler.kt} | 50 ++++++++++++----- .../e/apps/utils/NotificationUtils.kt | 55 ------------------- app/src/main/res/values/strings.xml | 5 +- 5 files changed, 62 insertions(+), 76 deletions(-) rename app/src/main/java/foundation/e/apps/utils/{AppsUncaughtExceptionHandler.kt => CustomUncaughtExceptionHandler.kt} (58%) delete mode 100644 app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt diff --git a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt index b8156fedb..2a4ee0989 100644 --- a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +++ b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt @@ -34,7 +34,7 @@ import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.install.workmanager.InstallWorkManager import foundation.e.apps.ui.setup.tos.TOS_VERSION -import foundation.e.apps.utils.AppsUncaughtExceptionHandler +import foundation.e.apps.utils.CustomUncaughtExceptionHandler import foundation.e.lib.telemetry.Telemetry import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.MainScope @@ -61,7 +61,7 @@ class AppLoungeApplication : Application(), Configuration.Provider { lateinit var preferenceManagerModule: PreferenceManagerModule @Inject - lateinit var uncaughtExceptionHandler: AppsUncaughtExceptionHandler + lateinit var uncaughtExceptionHandler: CustomUncaughtExceptionHandler @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate() { diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt index b31d74cb6..286c2727d 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt @@ -18,7 +18,11 @@ package foundation.e.apps.install.workmanager +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo @@ -27,7 +31,6 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R -import foundation.e.apps.utils.NotificationUtils import java.util.concurrent.atomic.AtomicInteger @HiltWorker @@ -69,15 +72,30 @@ class InstallAppWorker @AssistedInject constructor( private fun createForegroundInfo(progress: String): ForegroundInfo { val title = applicationContext.getString(R.string.app_name) val cancel = applicationContext.getString(R.string.cancel) + val channelId = context.getString(R.string.basic_notification_channel_id) + // This PendingIntent can be used to cancel the worker val intent = WorkManager.getInstance(applicationContext) .createCancelPendingIntent(id) - NotificationUtils.createBasicNotificationChannel(context) + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as + NotificationManager + // Create a Notification channel if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mChannel = NotificationChannel( + channelId, + title, + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(mChannel) + } - val notification = NotificationUtils.getBasicNotificationBuilder(context) + val notification = NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle(title) .setTicker(title) .setContentText(progress) + .setSmallIcon(R.drawable.app_lounge_notification_icon) .setOngoing(true) // Add the cancel action to the notification which can // be used to cancel the worker diff --git a/app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt b/app/src/main/java/foundation/e/apps/utils/CustomUncaughtExceptionHandler.kt similarity index 58% rename from app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt rename to app/src/main/java/foundation/e/apps/utils/CustomUncaughtExceptionHandler.kt index 25c4980a7..b280350f8 100644 --- a/app/src/main/java/foundation/e/apps/utils/AppsUncaughtExceptionHandler.kt +++ b/app/src/main/java/foundation/e/apps/utils/CustomUncaughtExceptionHandler.kt @@ -20,9 +20,12 @@ package foundation.e.apps.utils import android.Manifest import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context import android.content.pm.PackageManager import android.database.sqlite.SQLiteFullException +import android.os.Build import androidx.annotation.StringRes import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat @@ -34,7 +37,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AppsUncaughtExceptionHandler @Inject constructor( +class CustomUncaughtExceptionHandler @Inject constructor( @ApplicationContext private val context: Context ) : Thread.UncaughtExceptionHandler { @@ -51,27 +54,44 @@ class AppsUncaughtExceptionHandler @Inject constructor( } private fun showNotification(@StringRes contentId: Int) { - with(NotificationManagerCompat.from(context)) { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - return - } - - NotificationUtils.createBasicNotificationChannel(context) - notify(NOTIFICATION_ID, getNotification(contentId)) + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return } + + createNotificationChannel() + + val notification = getNotification(contentId) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification) } private fun getNotification(@StringRes contentId: Int): Notification { val content = context.getString(contentId) + val channelId = context.getString(R.string.warning_notification_channel_id) + val title = context.getString(R.string.app_name) - return NotificationUtils.getBasicNotificationBuilder(context) + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.app_lounge_notification_icon) + .setContentTitle(title) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) - .setContentText(content) - .build() + .setContentText(content).build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val id = context.getString(R.string.warning_notification_channel_id) + val title = context.getString(R.string.warning_notification_channel_title) + + val channel = NotificationChannel( + id, title, NotificationManager.IMPORTANCE_DEFAULT + ) + + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } } } diff --git a/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt b/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt deleted file mode 100644 index b9c8ff1a8..000000000 --- a/app/src/main/java/foundation/e/apps/utils/NotificationUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.os.Build -import androidx.core.app.NotificationCompat -import foundation.e.apps.R - -object NotificationUtils { - - fun createBasicNotificationChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val id = context.getString(R.string.generic_notification_channel_id) - val title = context.getString(R.string.app_name) - - val channel = NotificationChannel( - id, - title, - NotificationManager.IMPORTANCE_LOW - ) - - val notificationManager: NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - } - - fun getBasicNotificationBuilder(context: Context): NotificationCompat.Builder { - val channelId = context.getString(R.string.generic_notification_channel_id) - val title = context.getString(R.string.app_name) - - return NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.app_lounge_notification_icon) - .setContentTitle(title) - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 053c733af..3b2fd32fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,10 @@ App Lounge - applounge_notification + applounge_notification + + applounge_warning_notification + Warning Home -- GitLab