diff --git a/auto/build.gradle b/auto/build.gradle index 490c2650..e5c6de95 100644 --- a/auto/build.gradle +++ b/auto/build.gradle @@ -13,8 +13,8 @@ android { applicationId "de.michelinside.glucodataauto" minSdk rootProject.minSdk targetSdk rootProject.targetSdk - versionCode 1025 - versionName "1.0-beta1" + versionCode 1026 + versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -73,12 +73,12 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.joaomgcd:taskerpluginlibrary:0.4.4' implementation project(path: ':common') - implementation "androidx.car.app:app:1.2.0" + implementation "androidx.car.app:app:1.4.0" implementation "androidx.preference:preference:1.2.1" implementation "com.jaredrummler:colorpicker:1.1.0" implementation "androidx.media:media:1.7.0" diff --git a/auto/src/main/AndroidManifest.xml b/auto/src/main/AndroidManifest.xml index 1b7a4cbd..9da27997 100644 --- a/auto/src/main/AndroidManifest.xml +++ b/auto/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ - + @@ -106,6 +106,7 @@ + = Build.VERSION_CODES.S) { + // If you don't want to adapt the device's theme settings, uncomment the snippet below + val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode( + when (colorScheme) { + "light" -> UiModeManager.MODE_NIGHT_NO // User set this explicitly + "dark" -> UiModeManager.MODE_NIGHT_YES // User set this explicitly + else -> UiModeManager.MODE_NIGHT_AUTO // Follow the device Dark Theme settings when not define yet by user + } + ) + } else { + AppCompatDelegate.setDefaultNightMode( + when (colorScheme) { + "light" -> AppCompatDelegate.MODE_NIGHT_NO // User set this explicitly + "dark" -> AppCompatDelegate.MODE_NIGHT_YES // User set this explicitly + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM // For Android 10 and 11, follow the device Dark Theme settings when not define yet by user + } + ) + + } + } +} \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/GlucoDataServiceAuto.kt b/auto/src/main/java/de/michelinside/glucodataauto/GlucoDataServiceAuto.kt index 0df8bd2c..50463dde 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/GlucoDataServiceAuto.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/GlucoDataServiceAuto.kt @@ -13,6 +13,7 @@ import androidx.car.app.connection.CarConnection import de.michelinside.glucodataauto.android_auto.CarMediaBrowserService import de.michelinside.glucodataauto.android_auto.CarNotification import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.GdhUncaughtExecptionHandler import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.notification.ChannelType @@ -25,6 +26,7 @@ import de.michelinside.glucodatahandler.common.tasks.TimeTaskService import de.michelinside.glucodatahandler.common.utils.PackageUtils class GlucoDataServiceAuto: Service() { + companion object { private const val LOG_ID = "GDH.AA.GlucoDataServiceAuto" private var isForegroundService = false @@ -35,36 +37,45 @@ class GlucoDataServiceAuto: Service() { private var dataSyncCount = 0 val connected: Boolean get() = car_connected || CarMediaBrowserService.active fun init(context: Context) { + Log.v(LOG_ID, "init called: init=$init") if(!init) { - Log.v(LOG_ID, "init called") GlucoDataService.context = context - //TimeTaskService.useWorker = true - //SourceTaskService.useWorker = true - ReceiveData.initData(context) - CarConnection(context.applicationContext).type.observeForever(GlucoDataServiceAuto::onConnectionStateUpdated) + CarNotification.initNotification(context) + startService(context, false) init = true } } - private fun setForeground(context: Context, foreground: Boolean) { + private fun startService(context: Context, foreground: Boolean) { + try { + val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + val isForeground = foreground || sharedPref.getBoolean(Constants.SHARED_PREF_FOREGROUND_SERVICE, false) + val serviceIntent = Intent(context, GlucoDataServiceAuto::class.java) + serviceIntent.putExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, isForeground) + if (isForeground) + context.startForegroundService(serviceIntent) + else + context.startService(serviceIntent) + } catch (exc: Exception) { + Log.e(LOG_ID, "startService exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) + } + } + + fun setForeground(context: Context, foreground: Boolean) { try { Log.v(LOG_ID, "setForeground called " + foreground) if (isForegroundService != foreground) { - val serviceIntent = Intent(context, GlucoDataServiceAuto::class.java) - serviceIntent.putExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, foreground) - if (foreground) - context.startForegroundService(serviceIntent) - else - context.startService(serviceIntent) + startService(context, foreground) } } catch (exc: Exception) { - Log.e(LOG_ID, "setForeground exception: " + exc.toString()) + Log.e(LOG_ID, "setForeground exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } } fun start(context: Context) { try { if(!running) { + init(context) Log.i(LOG_ID, "starting") CarNotification.enable(context) startDataSync(context) @@ -72,7 +83,7 @@ class GlucoDataServiceAuto: Service() { running = true } } catch (exc: Exception) { - Log.e(LOG_ID, "start exception: " + exc.toString()) + Log.e(LOG_ID, "start exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } } @@ -86,34 +97,36 @@ class GlucoDataServiceAuto: Service() { running = false } } catch (exc: Exception) { - Log.e(LOG_ID, "stop exception: " + exc.toString()) + Log.e(LOG_ID, "stop exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } } fun startDataSync(context: Context) { try { + Log.i(LOG_ID, "starting datasync - count=$dataSyncCount") if (dataSyncCount == 0) { - Log.d(LOG_ID, "startDataSync count: $dataSyncCount") TimeTaskService.run(context) SourceTaskService.run(context) sendStateBroadcast(context, true) + Log.i(LOG_ID, "Datasync started") } dataSyncCount++ } catch (exc: Exception) { - Log.e(LOG_ID, "startDataSync exception: " + exc.toString()) + Log.e(LOG_ID, "startDataSync exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } } fun stopDataSync(context: Context) { try { dataSyncCount-- + Log.i(LOG_ID, "stopping datasync - count=$dataSyncCount") if (dataSyncCount == 0) { - Log.d(LOG_ID, "stopDataSync") sendStateBroadcast(context, false) BackgroundWorker.stopAllWork(context) + Log.i(LOG_ID, "Datasync stopped") } } catch (exc: Exception) { - Log.e(LOG_ID, "stopDataSync exception: " + exc.toString()) + Log.e(LOG_ID, "stopDataSync exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } } @@ -161,11 +174,16 @@ class GlucoDataServiceAuto: Service() { } @RequiresApi(Build.VERSION_CODES.Q) - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { try { Log.d(LOG_ID, "onStartCommand called") + GdhUncaughtExecptionHandler.init() super.onStartCommand(intent, flags, startId) - val isForeground = intent.getBooleanExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, false) + GlucoDataService.context = applicationContext + ReceiveData.initData(applicationContext) + CarNotification.initNotification(this) + val sharedPref = getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + val isForeground = (if(intent != null) intent.getBooleanExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, false) else false) || sharedPref.getBoolean(Constants.SHARED_PREF_FOREGROUND_SERVICE, false) if (isForeground && !isForegroundService) { Log.i(LOG_ID, "Starting service in foreground!") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) @@ -178,12 +196,12 @@ class GlucoDataServiceAuto: Service() { Log.i(LOG_ID, "Stopping service in foreground!") stopForeground(STOP_FOREGROUND_REMOVE) } + CarConnection(applicationContext).type.observeForever(GlucoDataServiceAuto::onConnectionStateUpdated) } catch (exc: Exception) { - Log.e(LOG_ID, "onStartCommand exception: " + exc.toString()) + Log.e(LOG_ID, "onStartCommand exception: " + exc.message.toString() + "\n" + exc.stackTraceToString()) } - if (isForegroundService) - return START_STICKY // keep alive - return START_NOT_STICKY + + return START_STICKY // keep alive } override fun onDestroy() { @@ -202,7 +220,7 @@ class GlucoDataServiceAuto: Service() { val pendingIntent = PackageUtils.getAppIntent(this, MainActivity::class.java, 11, false) return Notification.Builder(this, ChannelType.ANDROID_AUTO_FOREGROUND.channelId) - .setContentTitle(getString(de.michelinside.glucodatahandler.common.R.string.activity_main_car_connected_label)) + .setContentTitle(getString(de.michelinside.glucodatahandler.common.R.string.gda_foreground_info)) .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .setOngoing(true) diff --git a/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt b/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt index d8558d79..0f5870a6 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt @@ -21,7 +21,6 @@ import android.widget.ImageView import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -38,6 +37,8 @@ import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmType +import de.michelinside.glucodatahandler.common.notification.ChannelType +import de.michelinside.glucodatahandler.common.notification.Channels import de.michelinside.glucodatahandler.common.notifier.DataSource import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface @@ -69,7 +70,6 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var btnSources: Button private lateinit var txtNoData: TextView private lateinit var sharedPref: SharedPreferences - private var menuOpen = false private var notificationIcon: MenuItem? = null private val LOG_ID = "GDH.AA.Main" private var requestNotificationPermission = false @@ -141,11 +141,10 @@ class MainActivity : AppCompatActivity(), NotifierInterface { override fun onPause() { try { + Log.d(LOG_ID, "onPause called") super.onPause() InternalNotifier.remNotifier(this, this) - if(!menuOpen) - GlucoDataServiceAuto.stopDataSync(this) - Log.v(LOG_ID, "onPause called") + GlucoDataServiceAuto.stopDataSync(this) } catch (exc: Exception) { Log.e(LOG_ID, "onPause exception: " + exc.message.toString() ) } @@ -153,8 +152,9 @@ class MainActivity : AppCompatActivity(), NotifierInterface { override fun onResume() { try { + Log.d(LOG_ID, "onResume called") super.onResume() - Log.v(LOG_ID, "onResume called") + checkUncaughtException() update() InternalNotifier.addNotifier( this, this, mutableSetOf( NotifySource.BROADCAST, @@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { NotifySource.NODE_BATTERY_LEVEL, NotifySource.SETTINGS, NotifySource.CAR_CONNECTION, - NotifySource.OBSOLETE_VALUE, + NotifySource.TIME_VALUE, NotifySource.SOURCE_STATE_CHANGE)) checkExactAlarmPermission() checkBatteryOptimization() @@ -175,21 +175,12 @@ class MainActivity : AppCompatActivity(), NotifierInterface { requestNotificationPermission = false txtNotificationPermission.visibility = View.GONE } - if(!menuOpen) - GlucoDataServiceAuto.startDataSync(this) - menuOpen = false + GlucoDataServiceAuto.startDataSync(this) } catch (exc: Exception) { Log.e(LOG_ID, "onResume exception: " + exc.message.toString() ) } } - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - Log.d(LOG_ID, "Notification permission allowed: $isGranted") - } - fun requestPermission() : Boolean { requestNotificationPermission = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -257,16 +248,16 @@ class MainActivity : AppCompatActivity(), NotifierInterface { try { val pm = getSystemService(POWER_SERVICE) as PowerManager if (!pm.isIgnoringBatteryOptimizations(packageName)) { - Log.w(LOG_ID, "Battery optimization is inactive") + Log.w(LOG_ID, "Battery optimization is active") txtBatteryOptimization.visibility = View.VISIBLE txtBatteryOptimization.setOnClickListener { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) intent.data = Uri.parse("package:$packageName") startActivity(intent) } } else { txtBatteryOptimization.visibility = View.GONE - Log.i(LOG_ID, "Battery optimization is active") + Log.i(LOG_ID, "Battery optimization is inactive") } } catch (exc: Exception) { Log.e(LOG_ID, "checkBatteryOptimization exception: " + exc.message.toString() ) @@ -292,7 +283,15 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private fun updateNotificationIcon() { try { if(notificationIcon != null) { - val enabled = sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) + var enabled = sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) + if(enabled && !Channels.notificationChannelActive(this, ChannelType.ANDROID_AUTO)) { + Log.i(LOG_ID, "Disable car notification as there is no permission!") + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) + apply() + } + enabled = false + } notificationIcon!!.icon = ContextCompat.getDrawable(this, if(enabled) R.drawable.icon_popup_white else R.drawable.icon_popup_off_white) } } catch (exc: Exception) { @@ -305,25 +304,28 @@ class MainActivity : AppCompatActivity(), NotifierInterface { Log.v(LOG_ID, "onOptionsItemSelected for " + item.itemId.toString()) when(item.itemId) { R.id.action_settings -> { - menuOpen = true val intent = Intent(this, SettingsActivity::class.java) startActivity(intent) return true } R.id.action_sources -> { - menuOpen = true val intent = Intent(this, SettingsActivity::class.java) intent.putExtra(SettingsActivity.FRAGMENT_EXTRA, SettingsFragmentClass.SORUCE_FRAGMENT.value) startActivity(intent) return true } R.id.action_alarms -> { - menuOpen = true val intent = Intent(this, SettingsActivity::class.java) intent.putExtra(SettingsActivity.FRAGMENT_EXTRA, SettingsFragmentClass.ALARM_FRAGMENT.value) startActivity(intent) return true } + R.id.action_gda_help -> { + val intent = Intent(this, SettingsActivity::class.java) + intent.putExtra(SettingsActivity.FRAGMENT_EXTRA, SettingsFragmentClass.HELP_FRAGMENT.value) + startActivity(intent) + return true + } R.id.action_help -> { val browserIntent = Intent( Intent.ACTION_VIEW, @@ -353,11 +355,24 @@ class MainActivity : AppCompatActivity(), NotifierInterface { } R.id.action_notification_toggle -> { Log.v(LOG_ID, "notification toggle") - with(sharedPref.edit()) { - putBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, !sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false)) - apply() + if(!sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) && !Channels.notificationChannelActive(this, ChannelType.ANDROID_AUTO)) + { + val intent: Intent = if (Channels.notificationActive(this)) { // only the channel is inactive! + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, ChannelType.ANDROID_AUTO.channelId) + } else { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName) + } + startActivity(intent) + } else { + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, !sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false)) + apply() + } + updateNotificationIcon() } - updateNotificationIcon() } else -> return super.onOptionsItemSelected(item) } @@ -527,4 +542,17 @@ class MainActivity : AppCompatActivity(), NotifierInterface { companion object { const val CREATE_FILE = 1 } + + private fun checkUncaughtException() { + Log.d(LOG_ID, "Check uncaught exception ${sharedPref.getBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false)}") + if(sharedPref.getBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false)) { + val excMsg = sharedPref.getString(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_MESSAGE, "") + Log.e(LOG_ID, "Uncaught exception detected last time: $excMsg") + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false) + apply() + } + Dialogs.showOkDialog(this, CR.string.app_crash_title, CR.string.app_crash_message, null) + } + } } \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarMediaBrowserService.kt b/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarMediaBrowserService.kt index e65b2439..e239f957 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarMediaBrowserService.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarMediaBrowserService.kt @@ -21,6 +21,8 @@ import androidx.media.MediaBrowserServiceCompat import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.ReceiveData +import de.michelinside.glucodatahandler.common.notification.ChannelType +import de.michelinside.glucodatahandler.common.notification.Channels import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource @@ -149,7 +151,11 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh try { Log.d(LOG_ID, "onLoadChildren for parent: " + parentId) if (MEDIA_ROOT_ID == parentId) { - result.sendResult(mutableListOf(createMediaItem(), createToggleItem())) + val items = mutableListOf(createMediaItem()) + if (Channels.notificationChannelActive(this, ChannelType.ANDROID_AUTO)) { + items.add(createToggleItem()) + } + result.sendResult(items) } else { result.sendResult(null) } @@ -160,7 +166,7 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { - Log.v(LOG_ID, "OnNotifyData called") + Log.d(LOG_ID, "OnNotifyData called for source $dataSource") try { notifyChildrenChanged(MEDIA_ROOT_ID) } catch (exc: Exception) { @@ -169,7 +175,7 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - Log.v(LOG_ID, "onSharedPreferenceChanged called for key " + key) + Log.d(LOG_ID, "onSharedPreferenceChanged called for key " + key) try { when(key) { Constants.SHARED_PREF_CAR_NOTIFICATION, diff --git a/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarNotification.kt b/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarNotification.kt index 25683b11..3cee6bf1 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarNotification.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/android_auto/CarNotification.kt @@ -106,7 +106,7 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC } } - private fun initNotification(context: Context) { + fun initNotification(context: Context) { try { if(!init) { Log.v(LOG_ID, "initNotification called") @@ -154,7 +154,8 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC private fun getFilter(): MutableSet { val filter = mutableSetOf( NotifySource.BROADCAST, - NotifySource.MESSAGECLIENT) + NotifySource.MESSAGECLIENT, + NotifySource.OBSOLETE_ALARM_TRIGGER) if (notification_reappear_interval > 0) filter.add(NotifySource.TIME_VALUE) return filter @@ -207,28 +208,32 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC + "\nnotification_interval: " + notification_interval + "\nnotification_reappear_interval: " + notification_reappear_interval ) - if (dataSource == NotifySource.BROADCAST || dataSource == NotifySource.MESSAGECLIENT) { + if (dataSource == NotifySource.OBSOLETE_ALARM_TRIGGER) { + Log.d(LOG_ID, "Obsolete alarm triggered") + forceNextNotify = true + return true + } else if (dataSource == NotifySource.BROADCAST || dataSource == NotifySource.MESSAGECLIENT) { if(notification_interval == 1L || ReceiveData.forceAlarm) { - Log.v(LOG_ID, "Notification has forced by interval or alarm") + Log.d(LOG_ID, "Notification has forced by interval or alarm") return true } if (ReceiveData.getAlarmType() == AlarmType.VERY_LOW) { - Log.v(LOG_ID, "Notification for very low-alarm") + Log.d(LOG_ID, "Notification for very low-alarm") forceNextNotify = true // if obsolete or VERY_LOW, the next value is important! return true } if (forceNextNotify) { - Log.v(LOG_ID, "Force notification") + Log.d(LOG_ID, "Force notification") forceNextNotify = false return true } if (notification_interval > 1L && getTimeDiffMinute() >= notification_interval && ReceiveData.getElapsedTimeMinute() == 0L) { - Log.v(LOG_ID, "Interval for new value elapsed") + Log.d(LOG_ID, "Interval for new value elapsed") return true } } else if(dataSource == NotifySource.TIME_VALUE) { if (notification_reappear_interval > 0 && ReceiveData.getElapsedTimeMinute().mod(notification_reappear_interval) == 0L) { - Log.v(LOG_ID, "reappear after: " + ReceiveData.getElapsedTimeMinute() + " - interval: " + notification_reappear_interval) + Log.d(LOG_ID, "reappear after: " + ReceiveData.getElapsedTimeMinute() + " - interval: " + notification_reappear_interval) return true } } diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/AlarmFragment.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/AlarmFragment.kt index bf460376..c776f2f2 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/AlarmFragment.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/AlarmFragment.kt @@ -3,6 +3,7 @@ package de.michelinside.glucodataauto.preferences import android.annotation.SuppressLint import android.os.Bundle import android.util.Log +import androidx.core.content.ContextCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import de.michelinside.glucodataauto.R @@ -82,6 +83,14 @@ class AlarmFragment : PreferenceFragmentCompat() { val pref = findPreference(key) ?: return val alarmType = AlarmType.fromIndex(pref.extras.getInt("type")) pref.summary = getAlarmCatSummary(alarmType) + pref.icon = ContextCompat.getDrawable(requireContext(), getAlarmCatIcon(alarmType, key + "_enabled")) + } + + private fun getAlarmCatIcon(alarmType: AlarmType, enableKey: String): Int { + if(alarmType != AlarmType.VERY_LOW && !preferenceManager.sharedPreferences!!.getBoolean(enableKey, true)) { + return R.drawable.icon_popup_off + } + return R.drawable.icon_popup } private fun getAlarmCatSummary(alarmType: AlarmType): String { diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/AppSettingsActivity.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/AppSettingsActivity.kt index a4e20657..8acaefc9 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/AppSettingsActivity.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/AppSettingsActivity.kt @@ -12,7 +12,8 @@ import de.michelinside.glucodatahandler.common.R as RC enum class SettingsFragmentClass(val value: Int, val titleRes: Int) { SETTINGS_FRAGMENT(0, RC.string.menu_settings), SORUCE_FRAGMENT(1, RC.string.menu_sources), - ALARM_FRAGMENT(2, RC.string.menu_alarms) + ALARM_FRAGMENT(2, RC.string.menu_alarms), + HELP_FRAGMENT(3, RC.string.menu_help), } class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { @@ -51,6 +52,14 @@ class SettingsActivity : AppCompatActivity(), .replace(R.id.content, AlarmFragment()) .commit() } + + SettingsFragmentClass.HELP_FRAGMENT.value -> { + this.supportActionBar!!.title = + this.applicationContext.resources.getText(SettingsFragmentClass.HELP_FRAGMENT.titleRes) + supportFragmentManager.beginTransaction() + .replace(R.id.content, HelpFragment()) + .commit() + } } } diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/HelpFragment.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/HelpFragment.kt new file mode 100644 index 00000000..031d2262 --- /dev/null +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/HelpFragment.kt @@ -0,0 +1,22 @@ +package de.michelinside.glucodataauto.preferences + +import android.os.Bundle +import android.util.Log +import androidx.preference.* +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodataauto.R + + +class HelpFragment() : PreferenceFragmentCompat() { + protected val LOG_ID = "GDH.AA.HelpFragment" + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + Log.d(LOG_ID, "onCreatePreferences called") + try { + preferenceManager.sharedPreferencesName = Constants.SHARED_PREF_TAG + setPreferencesFromResource(R.xml.help, rootKey) + } catch (exc: Exception) { + Log.e(LOG_ID, "onCreatePreferences exception: " + exc.toString()) + } + } +} \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt index 7caab0e9..84f96632 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt @@ -1,14 +1,16 @@ package de.michelinside.glucodataauto.preferences +import android.content.SharedPreferences import android.os.Bundle import android.util.Log import androidx.preference.* import de.michelinside.glucodataauto.BuildConfig +import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodataauto.R import de.michelinside.glucodatahandler.common.Constants -class SettingsFragment : PreferenceFragmentCompat() { +class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { private val LOG_ID = "GDH.AA.SettingsFragment" override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -27,4 +29,38 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + + override fun onResume() { + Log.d(LOG_ID, "onResume called") + try { + super.onResume() + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } catch (exc: Exception) { + Log.e(LOG_ID, "onResume exception: " + exc.toString()) + } + } + + override fun onPause() { + Log.d(LOG_ID, "onPause called") + try { + super.onPause() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } catch (exc: Exception) { + Log.e(LOG_ID, "onPause exception: " + exc.toString()) + } + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + Log.d(LOG_ID, "onSharedPreferenceChanged called for " + key) + try { + when(key) { + Constants.SHARED_PREF_FOREGROUND_SERVICE -> { + GlucoDataServiceAuto.setForeground(requireContext(), sharedPreferences!!.getBoolean(Constants.SHARED_PREF_FOREGROUND_SERVICE, false)) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onSharedPreferenceChanged exception: " + exc.toString()) + } + } + } diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragmentBase.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragmentBase.kt index b4dfe19b..b8c28cc5 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragmentBase.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragmentBase.kt @@ -1,16 +1,20 @@ package de.michelinside.glucodataauto.preferences +import android.content.Intent import android.content.SharedPreferences import android.os.Bundle +import android.provider.Settings import android.util.Log import androidx.core.content.ContextCompat import androidx.preference.* import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodataauto.R +import de.michelinside.glucodatahandler.common.notification.ChannelType +import de.michelinside.glucodatahandler.common.notification.Channels abstract class SettingsFragmentBase(private val prefResId: Int) : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { - protected val LOG_ID = "GDH.SettingsFragmentBase" + protected val LOG_ID = "GDH.AA.SettingsFragmentBase" private val updateEnablePrefs = mutableSetOf() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -107,13 +111,34 @@ class GDASettingsFragment: SettingsFragmentBase(R.xml.pref_gda) { override fun updatePreferences() { super.updatePreferences() val pref = findPreference(Constants.SHARED_PREF_CAR_NOTIFICATION) ?: return + if (pref.isChecked && !Channels.notificationChannelActive(requireContext(), ChannelType.ANDROID_AUTO)) { + Log.i(LOG_ID, "Disable car notification as there is no permission!") + pref.isChecked = false + with(preferenceManager.sharedPreferences!!.edit()) { + putBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) + apply() + } + } pref.icon = ContextCompat.getDrawable(requireContext(), if(pref.isChecked) R.drawable.icon_popup else R.drawable.icon_popup_off) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { super.onSharedPreferenceChanged(sharedPreferences, key) - if(key == Constants.SHARED_PREF_CAR_NOTIFICATION) - updatePreferences() + if(key == Constants.SHARED_PREF_CAR_NOTIFICATION) { + if (preferenceManager.sharedPreferences!!.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, false) && !Channels.notificationChannelActive(requireContext(), ChannelType.ANDROID_AUTO)) { + val intent: Intent = if (Channels.notificationActive(requireContext())) { // only the channel is inactive! + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, ChannelType.ANDROID_AUTO.channelId) + } else { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + } + startActivity(intent) + } else { + updatePreferences() + } + } } } \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SourceFragment.kt b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SourceFragment.kt index 07a116f3..f0dede6c 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SourceFragment.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SourceFragment.kt @@ -61,6 +61,7 @@ class SourceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPre preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) updateEnableStates(preferenceManager.sharedPreferences!!) InternalNotifier.addNotifier(requireContext(), this, mutableSetOf(NotifySource.PATIENT_DATA_CHANGED)) + //GlucoDataServiceAuto.startDataSync(requireContext()) super.onResume() } catch (exc: Exception) { Log.e(LOG_ID, "onResume exception: " + exc.toString()) @@ -72,6 +73,7 @@ class SourceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPre try { preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) InternalNotifier.remNotifier(requireContext(), this) + //GlucoDataServiceAuto.stopDataSync(requireContext()) super.onPause() } catch (exc: Exception) { Log.e(LOG_ID, "onPause exception: " + exc.toString()) diff --git a/auto/src/main/res/layout-land/activity_main.xml b/auto/src/main/res/layout-land/activity_main.xml index e01487e5..d7ae5bc5 100644 --- a/auto/src/main/res/layout-land/activity_main.xml +++ b/auto/src/main/res/layout-land/activity_main.xml @@ -157,7 +157,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" - android:text="@string/activity_main_battery_optimization_disabled" + android:text="@string/gda_battery_optimization_disabled" android:textSize="16sp" android:textAlignment="center" /> diff --git a/auto/src/main/res/layout/activity_main.xml b/auto/src/main/res/layout/activity_main.xml index c6b0f994..613d3c10 100644 --- a/auto/src/main/res/layout/activity_main.xml +++ b/auto/src/main/res/layout/activity_main.xml @@ -175,7 +175,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" - android:text="@string/activity_main_battery_optimization_disabled" + android:text="@string/gda_battery_optimization_disabled" android:textSize="16sp" android:textAlignment="center" /> diff --git a/auto/src/main/res/menu/menu_items.xml b/auto/src/main/res/menu/menu_items.xml index 7e66a17b..ffe988d8 100644 --- a/auto/src/main/res/menu/menu_items.xml +++ b/auto/src/main/res/menu/menu_items.xml @@ -29,6 +29,13 @@ android:title="@string/menu_alarms" app:showAsAction="never" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/auto/src/main/res/xml/preferences.xml b/auto/src/main/res/xml/preferences.xml index 0930195b..0bfcac8d 100644 --- a/auto/src/main/res/xml/preferences.xml +++ b/auto/src/main/res/xml/preferences.xml @@ -1,6 +1,12 @@ + + use separate tag for not trigger onChanged events const val SHARED_PREF_INTERNAL_TAG = "GlucoDataHandlerInternalAppPrefs" @@ -116,7 +119,6 @@ object Constants { const val WIDGET_STYLE_GLUCOSE_TREND_TIME_DELTA_IOB_COB = "glucose_trend_delta_time_iob_cob" // Wear only preferences - const val SHARED_PREF_FOREGROUND_SERVICE = "foreground_service" const val SHARED_PREF_WEAR_COLORED_AOD = "colored_aod" const val SHARED_PREF_COMPLICATION_TAP_ACTION = "complication_tap_action" @@ -219,4 +221,5 @@ object Constants { // Android Auto const val AA_MEDIA_ICON_STYLE_TREND = "trend" const val AA_MEDIA_ICON_STYLE_GLUCOSE_TREND = "glucose_trend" + const val SHARED_PREF_FOREGROUND_SERVICE = "foreground_service" } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/GdhUncaughtExecptionHandler.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/GdhUncaughtExecptionHandler.kt new file mode 100644 index 00000000..9318754f --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/GdhUncaughtExecptionHandler.kt @@ -0,0 +1,69 @@ +package de.michelinside.glucodatahandler.common + +import android.util.Log +import java.lang.Exception +import kotlin.system.exitProcess + +object GdhUncaughtExecptionHandler : Thread.UncaughtExceptionHandler { + private val LOG_ID = "GDH.UncaughtExceptionHandler" + private var defaultHandler: Thread.UncaughtExceptionHandler? = null + private var exceptionCaught = false + + fun init() { + Log.d(LOG_ID, "init called") + if(defaultHandler == null) { + defaultHandler = Thread.getDefaultUncaughtExceptionHandler()!! + Log.d(LOG_ID, "Replace default handler $defaultHandler") + Thread.setDefaultUncaughtExceptionHandler(this) + } + } + + override fun uncaughtException(t: Thread, e: Throwable) { + try { + if(!exceptionCaught) { + exceptionCaught = true + val message = "${e.message}\n${e.stackTraceToString()}" + Log.e(LOG_ID, "Uncaught exception detected in thread ${t.name}: $message") + if(GlucoDataService.sharedPref != null) { + val sharedPref = GlucoDataService.sharedPref!! + val customLayoutEnabled = sharedPref.getBoolean( + Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT, + true + ) + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, true) + putString(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_MESSAGE, message) + if (message.contains("BadForegroundServiceNotificationException") || message.contains( + "RemoteServiceException" + ) + ) { + Log.e( + LOG_ID, + "BadForegroundServiceNotificationException detected! customLayoutEnabled=$customLayoutEnabled" + ) + if (customLayoutEnabled) + putBoolean( + Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT, + false + ) + else + putBoolean(Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY, true) + } + apply() + Thread.sleep(100) // wait for saving + Log.v(LOG_ID, "Exception saved!") + } + } + } else { + Log.d(LOG_ID, "Exception already handled!") + } + } catch (e: Exception) { + Log.e(LOG_ID, "Exception: ${e.message}") + } + if(defaultHandler!=null) + defaultHandler!!.uncaughtException(t, e) + else + exitProcess(10) + } + +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt index a2392279..3e4aaeb8 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt @@ -137,6 +137,7 @@ abstract class GlucoDataService(source: AppSource) : WearableListenerService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { try { Log.v(LOG_ID, "onStartCommand called") + GdhUncaughtExecptionHandler.init() super.onStartCommand(intent, flags, startId) val isForeground = true // intent?.getBooleanExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, true) --> always use foreground!!! if (isForeground && !isForegroundService) { @@ -157,6 +158,7 @@ abstract class GlucoDataService(source: AppSource) : WearableListenerService() { return START_STICKY // keep alive } + @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onCreate() { try { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmHandler.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmHandler.kt index e96d605b..d2aa9fe1 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmHandler.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmHandler.kt @@ -1,7 +1,12 @@ package de.michelinside.glucodatahandler.common.notification +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent import android.content.SharedPreferences +import android.os.Build import android.os.Bundle import android.util.Log import de.michelinside.glucodatahandler.common.Command @@ -39,6 +44,9 @@ object AlarmHandler: SharedPreferences.OnSharedPreferenceChangeListener, Notifie private var snoozeTime = 0L private lateinit var sharedExtraPref: SharedPreferences + private var alarmManager: AlarmManager? = null + private var snoozeEndPendingIntent: PendingIntent? = null + val isSnoozeActive: Boolean get() { return snoozeTime >= System.currentTimeMillis() } @@ -112,10 +120,11 @@ object AlarmHandler: SharedPreferences.OnSharedPreferenceChangeListener, Notifie fun setSnoozeTime(time: Long, fromClient: Boolean = false) { snoozeTime = time - Log.d(LOG_ID, "New snooze-time: $snoozeTimestamp") + Log.i(LOG_ID, "New snooze-time: $snoozeTimestamp") saveExtras() if(GlucoDataService.context != null) { InternalNotifier.notify(GlucoDataService.context!!, NotifySource.ALARM_STATE_CHANGED, null) + triggerSnoozeEnd(GlucoDataService.context!!) } if(!fromClient) { val bundle = Bundle() @@ -334,4 +343,66 @@ object AlarmHandler: SharedPreferences.OnSharedPreferenceChangeListener, Notifie } } } + + fun triggerSnoozeEnd(context: Context) { + stopSnoozeEnd() + if(isSnoozeActive) { + alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + var hasExactAlarmPermission = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (!alarmManager!!.canScheduleExactAlarms()) { + Log.d(LOG_ID, "Need permission to set exact alarm!") + hasExactAlarmPermission = false + } + } + val intent = Intent(context, AlarmSnoozeEndReceiver::class.java) + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + snoozeEndPendingIntent = PendingIntent.getBroadcast( + context, + 800, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + Log.i(LOG_ID, "Trigger SnoozeEnd at $snoozeTimestamp - exact-alarm=$hasExactAlarmPermission") + if (hasExactAlarmPermission) { + alarmManager!!.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + snoozeTime, + snoozeEndPendingIntent!! + ) + } else { + alarmManager!!.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + snoozeTime, + snoozeEndPendingIntent!! + ) + } + } + } + + fun stopSnoozeEnd() { + if(alarmManager != null && snoozeEndPendingIntent != null) { + Log.i(LOG_ID, "Stop SnoozeEnd triggered") + alarmManager!!.cancel(snoozeEndPendingIntent!!) + alarmManager = null + snoozeEndPendingIntent = null + } + } +} + + +class AlarmSnoozeEndReceiver: BroadcastReceiver() { + private val LOG_ID = "GDH.AlarmSnoozeEndReceiver" + override fun onReceive(context: Context, intent: Intent) { + try { + Log.d(LOG_ID, "onReceive called snoozeActive:${AlarmHandler.isSnoozeActive}") + if(!AlarmHandler.isSnoozeActive) { + Log.i(LOG_ID, "End of snooze reached") + InternalNotifier.notify(context, NotifySource.ALARM_STATE_CHANGED, null) + AlarmHandler.stopSnoozeEnd() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onReceive exception: " + exc.toString()) + } + } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmNotificationBase.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmNotificationBase.kt index 89312256..04eea980 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmNotificationBase.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/AlarmNotificationBase.kt @@ -60,7 +60,6 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha private var vibratorInstance: Vibrator? = null private var alarmManager: AlarmManager? = null private var alarmPendingIntent: PendingIntent? = null - private var alarmNotificationActive: Boolean = false private var useAlarmSound: Boolean = true private var currentAlarmState: AlarmState = AlarmState.DISABLED @@ -102,13 +101,10 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha if(state == AlarmState.DISABLED || !channelActive(context)) { state = AlarmState.DISABLED } else if(state == AlarmState.ACTIVE) { - if(!alarmNotificationActive) { - initNotifier(context) - } - if(!active || !alarmNotificationActive) { + if(!active) { Log.d( LOG_ID, - "Inactive causes by active: $active - notification-active: $alarmNotificationActive" + "Inactive causes by active: $active" ) state = AlarmState.INACTIVE } @@ -120,10 +116,6 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha return state } - private fun isAlarmActive(context: Context): Boolean { - return AlarmState.currentState(context) == AlarmState.ACTIVE - } - fun initNotifications(context: Context) { try { Log.v(LOG_ID, "initNotifications called") @@ -133,12 +125,25 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) sharedPref.registerOnSharedPreferenceChangeListener(this) onSharedPreferenceChanged(sharedPref, null) - initNotifier() + initNotifier(context) } catch (exc: Exception) { Log.e(LOG_ID, "initNotifications exception: " + exc.toString() ) } } + open fun getNotifierFilter(): MutableSet { + return mutableSetOf() + } + + fun initNotifier(context: Context) { + Log.v(LOG_ID, "initNotifier called") + val filter = mutableSetOf(NotifySource.ALARM_STATE_CHANGED) + filter.add(NotifySource.ALARM_TRIGGER) + filter.add(NotifySource.OBSOLETE_ALARM_TRIGGER) + filter.addAll(getNotifierFilter()) + InternalNotifier.addNotifier(context, this, filter ) + } + fun getEnabled(): Boolean = enabled private fun setEnabled(newEnabled: Boolean) { @@ -147,7 +152,6 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha if (enabled != newEnabled) { enabled = newEnabled Log.i(LOG_ID, "enable alarm notifications: $newEnabled") - initNotifier() if(!enabled) { stopCurrentNotification() } @@ -568,12 +572,7 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha } fun channelActive(context: Context): Boolean { - if(Utils.checkPermission(context, android.Manifest.permission.POST_NOTIFICATIONS, Build.VERSION_CODES.TIRAMISU)) { - val channel = Channels.getNotificationManager(context).getNotificationChannel(getChannelId()) - Log.d(LOG_ID, "Channel: prio=${channel.importance}") - return (channel.importance > NotificationManager.IMPORTANCE_NONE) - } - return false + return Channels.notificationChannelActive(context, getChannel()) } protected fun checkRecreateSound() { @@ -610,8 +609,12 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha } } + fun getChannel(): ChannelType { + return ChannelType.ALARM + } + fun getChannelId(): String { - return ChannelType.ALARM.channelId + return getChannel().channelId } fun getAlarmSoundRes(alarmType: AlarmType): Int? { @@ -761,14 +764,13 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha Log.d(LOG_ID, "OnNotifyData called for $dataSource") when(dataSource) { NotifySource.ALARM_TRIGGER -> { - if (ReceiveData.forceAlarm) - triggerNotification(ReceiveData.getAlarmType(), context) + triggerNotification(ReceiveData.getAlarmType(), context) } NotifySource.OBSOLETE_ALARM_TRIGGER -> { triggerNotification(AlarmType.OBSOLETE, context) } NotifySource.ALARM_STATE_CHANGED -> { - initNotifier(context) + getAlarmState(context) } else -> Log.w(LOG_ID, "Unsupported source $dataSource") } @@ -803,27 +805,6 @@ abstract class AlarmNotificationBase: NotifierInterface, SharedPreferences.OnSha } } - open fun getNotifierFilter(): MutableSet { - return mutableSetOf() - } - - fun initNotifier(context: Context? = null) { - val requireConext = context ?: GlucoDataService.context!! - val newActive = isAlarmActive(requireConext) - Log.v(LOG_ID, "initNotifier called for newActive: $newActive") - if(alarmNotificationActive != newActive) { - Log.i(LOG_ID, "Change alarm notification active to ${newActive}") - alarmNotificationActive = newActive - val filter = mutableSetOf(NotifySource.ALARM_STATE_CHANGED) - if(alarmNotificationActive) { - filter.add(NotifySource.ALARM_TRIGGER) - filter.add(NotifySource.OBSOLETE_ALARM_TRIGGER) - } - filter.addAll(getNotifierFilter()) - InternalNotifier.addNotifier(requireConext, this, filter ) - } - } - fun hasFullscreenPermission(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return Channels.getNotificationManager().canUseFullScreenIntent() diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/Channels.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/Channels.kt index 0feade11..587bc9ac 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notification/Channels.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notification/Channels.kt @@ -1,11 +1,15 @@ package de.michelinside.glucodatahandler.common.notification +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import android.os.Build +import android.util.Log import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.R +import de.michelinside.glucodatahandler.common.utils.Utils enum class ChannelType(val channelId: String, val nameResId: Int, val descrResId: Int, val importance: Int = NotificationManager.IMPORTANCE_DEFAULT) { MOBILE_FOREGROUND("GDH_foreground", R.string.mobile_foreground_notification_name, R.string.mobile_foreground_notification_descr ), @@ -17,6 +21,7 @@ enum class ChannelType(val channelId: String, val nameResId: Int, val descrResId ALARM("gdh_alarm_notification_channel", R.string.alarm_notification_name, R.string.alarm_notification_descr, NotificationManager.IMPORTANCE_MAX ); } object Channels { + private val LOG_ID = "GDH.Channels" private var notificationMgr: NotificationManager? = null private val obsoleteNotifications = mutableSetOf( @@ -60,4 +65,22 @@ object Channels { fun deleteNotificationChannel(context: Context, type: ChannelType) { getNotificationManager(context).deleteNotificationChannel(type.channelId) } + + @SuppressLint("InlinedApi") + fun notificationActive(context: Context): Boolean { + return Utils.checkPermission(context, android.Manifest.permission.POST_NOTIFICATIONS, Build.VERSION_CODES.TIRAMISU) + } + + fun notificationChannelActive(context: Context, type: ChannelType): Boolean { + if(notificationActive(context)) { + val channel = getNotificationManager(context).getNotificationChannel(type.channelId) + if (channel != null) { + Log.d(LOG_ID, "Channel: prio=${channel.importance}") + return (channel.importance > NotificationManager.IMPORTANCE_NONE) + } else { + Log.w(LOG_ID, "Notification channel $type still not exists!") + } + } + return false + } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/BackgroundTaskService.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/BackgroundTaskService.kt index 473b3412..e72292d5 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/BackgroundTaskService.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/BackgroundTaskService.kt @@ -66,7 +66,7 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: fun checkExecution(task: BackgroundTask? = null): Boolean { if(task != null) { - Log.v(LOG_ID, "checkExecution for " + task.javaClass.simpleName + ": elapsedTimeMinute=" + elapsedTimeMinute + Log.d(LOG_ID, "checkExecution for " + task.javaClass.simpleName + ": elapsedTimeMinute=" + elapsedTimeMinute + " - lastElapsedMinute=" + lastElapsedMinute + " - elapsedIobCobTimeMinute=" + elapsedIobCobTimeMinute + " - interval=" + task.getIntervalMinute() @@ -74,17 +74,17 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: if(task.active(elapsedTimeMinute)) { if (elapsedTimeMinute != 0L) { if (lastElapsedMinute < 0 && initialExecution) { - Log.v(LOG_ID, "Trigger initial task execution") + Log.d(LOG_ID, "Trigger initial task execution") return true // trigger initial execution } if (elapsedTimeMinute.mod(task.getIntervalMinute()) == 0L) { - Log.v(LOG_ID, "Trigger "+ task.javaClass.simpleName + " execution after " + elapsedTimeMinute + " min") + Log.d(LOG_ID, "Trigger "+ task.javaClass.simpleName + " execution after " + elapsedTimeMinute + " min") return true // interval expired for active task } } if (task.hasIobCobSupport()) { if (elapsedIobCobTimeMinute >= task.getIntervalMinute()) { - Log.v(LOG_ID, "Trigger " + task.javaClass.simpleName + " IOB/COB execution after " + elapsedIobCobTimeMinute + " min") + Log.d(LOG_ID, "Trigger " + task.javaClass.simpleName + " IOB/COB execution after " + elapsedIobCobTimeMinute + " min") return true // IOB/COB interval expired for active task } } @@ -94,11 +94,11 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: + " - lastElapsedMinute=" + lastElapsedMinute + " - elapsedIobCobTimeMinute=" + elapsedIobCobTimeMinute) if ((lastElapsedMinute != elapsedTimeMinute && elapsedTimeMinute != 0L)) { - Log.v(LOG_ID, "Check task execution after " + elapsedTimeMinute + " min") + Log.d(LOG_ID, "Check task execution after " + elapsedTimeMinute + " min") return true // time expired and no new value } if (hasIobCobSupport() && elapsedIobCobTimeMinute > 0) { - Log.v(LOG_ID, "Check IOB/COB task execution after " + elapsedIobCobTimeMinute + " min") + Log.d(LOG_ID, "Check IOB/COB task execution after " + elapsedIobCobTimeMinute + " min") return true // check each task for additional IOB COB data } } @@ -197,10 +197,11 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: return // not yet initialized val newInterval = getInterval() val newDelay = getDelay() - if (initialExecution || curInterval != newInterval || curDelay != newDelay) { - Log.i(LOG_ID, "Interval has changed from " + curInterval + "m+" + curDelay + "ms to " + newInterval + "m+" + newDelay + "ms") - var triggerExecute = initialExecution || (curInterval <= 0 && newInterval > 0) // changed from inactive to active so trigger an initial execution - if (!triggerExecute && curInterval > newInterval && elapsedTimeMinute >= newInterval) { + Log.d(LOG_ID, "checkTimer for current: ${curInterval}m+${curDelay}ms and new: ${newInterval}m+${newDelay}ms - initialExecution=$initialExecution") + if (curInterval != newInterval || curDelay != newDelay) { + Log.i(LOG_ID, "Interval has changed from ${curInterval}m+${curDelay}ms to ${newInterval}m+${newDelay}ms - initialExecution=$initialExecution") + var triggerExecute = initialExecution && (curInterval <= 0 && newInterval > 0) // changed from inactive to active so trigger an initial execution + if (!triggerExecute && newInterval > 0 && curInterval > newInterval && elapsedTimeMinute >= newInterval) { // interval get decreased, so check for execution is needed triggerExecute = true } @@ -317,13 +318,15 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: alarmManager!!.cancel(pendingIntent!!) alarmManager = null currentAlarmTime = 0L + curDelay = -1L + curInterval = -1L } } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { try { if (sharedPreferences != null) { - Log.v(LOG_ID, "onSharedPreferenceChanged called for " + key) + Log.d(LOG_ID, "onSharedPreferenceChanged called for " + key) var changed = false backgroundTaskList.forEach { if (it.checkPreferenceChanged(sharedPreferences, key, context!!)) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/DataSourceTask.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/DataSourceTask.kt index 31ebf139..91e31e7a 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/DataSourceTask.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/DataSourceTask.kt @@ -322,7 +322,7 @@ abstract class DataSourceTask(private val enabledKey: String, protected val sour } override fun checkPreferenceChanged(sharedPreferences: SharedPreferences, key: String?, context: Context): Boolean { - Log.v(LOG_ID, "checkPreferenceChanged for $key") + Log.d(LOG_ID, "checkPreferenceChanged for $key") if(key == null) { enabled = sharedPreferences.getBoolean(enabledKey, false) interval = sharedPreferences.getString(Constants.SHARED_PREF_SOURCE_INTERVAL, "1")?.toLong() ?: 1L diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 841f839b..6cccdb6e 100644 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -468,5 +468,22 @@ Bitte deaktivieren, wenn der Trendpfeil zu groß und dadurch abgeschnitten wird. Oberfläche Allgemeine Einstellung für die Oberfläche von dieser App. - + GlucoDataAuto wird ausgeführt + Aktiviere dies nur, wenn du eine Online Datenquelle und nur Android Auto-Benachrichtigungen verwendest.\nDies verhindert, dass Android diese App schließt, wenn sie nicht mit Android Auto verbunden ist, sodass die App die Android Auto-Verbindung nicht erkennen kann.\nAnstatt diese Einstellung zu aktivieren, kannst du die App auch einmal in Android Auto öffnen, wenn du verbunden bist. + Akkuoptimierungen sind aktiv!\nHier drücken, um es zu deaktivieren. + Wenn du eine Online Datenquelle verwendest, wird die App möglicherweise nicht gestartet, wenn du mit Android Auto verbunden bist und nur Android Auto-Benachrichtigungen verwendest.\nUm sicherzustellen, dass die App die Android Auto-Verbindung erkennt, musst du die Akkuoptimierung deaktivieren.\nWenn es immer noch nicht funktioniert, kannst du auch den Vordergrundmodus aktivieren oder die App einmal in Android Auto öffnen, wenn sie mit Android Auto verbunden ist. + Android Auto einrichten + Android Auto ist entweder eine separate App oder Bestandteil vom System und kann über die Android Einstellungen geöffnet werden.\nUm GlucoDataAuto in Android Auto zu verwenden, müssen die folgende Schritte durchgeführt werden: + 1. Entwicklereinstellungen aktivieren + - Android Auto öffnen\n- bis zu Version scrollen\n- mehrfach auf Version drücken bis ein Popup mit \"Entwicklereinstellungen zulassen\" kommt\n- \"OK\" drücken + 2. \"Unbekannte Quellen\" aktivieren + - Android Auto öffnen\n- in den 3-Punkt Menü \"Entwicklereinstellungen auswählen\"\n- bis zu \"Unbekannte Quellen\" scrollen und aktivieren + 3. Benachrichtigungen aktivieren + - Android Auto öffnen\n- bis zu \"Messaging\" scrollen\n- \"Benachrichtigungen für Nachrichten anzeigen\" aktivieren\n- \"Erste Zeile einer Nachricht anzeigen\" aktivieren + 4. GlucoDataAuto aktivieren + - Android Auto öffnen\n- bis zu \"Display\" scrollen\n- \"Launcher anpassen\" öffnen\n- \"GlucoDataAuto\" aktivieren\nIst GlucoDataAuto nicht verfügbar, bitte das Telefon neustarten. + Farbiges Layout + Zeigt die Werte in großer und farbiger Schrift.\nAuf einigen Samsung Geräte ab Android 13 kann dies zu einem Absturz der App führen, aufgrund eines Bugs auf Seite von Samsung.\nWenn das Problem auftritt, wird diese Einstellung automatisch deaktiviert, damit die App wieder lauffähig ist. + Absturz erkannt! + Es tut mir so leid, dass die App während der Ausführung abgestürzt ist!\nBitte sendet mir die Logs zu, dann kann ich den Fehler analysieren und versuchen so schnell wie möglich zu fixen.\nDanke! diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index fbab048b..678bdd81 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -483,4 +483,22 @@ Por favor, desactiva esta configuración si la flecha de tendencia es demasiado grande y se corta. Interfaz de usuario Ajustes generales para la interfaz de usuario de esta aplicación. + GlucoDataAuto se está ejecutando + Actívalo solo si estás usando una fuente de seguidor y solo notificaciones de Android Auto.\nEsto evita que Android cierre esta aplicación mientras no esté conectado a Android Auto, por lo que la aplicación no puede reconocer la conexión a Android Auto.\nEn lugar de habilitar esta configuración, también puedes abrir la aplicación en Android Auto una vez, si estás conectado. + La optimización de batería está habilitada!\nPulsa aquí para desactivarlo. + Si estás usando una fuente de seguidor, la aplicación puede no iniciarse mientras estés conectado a Android Auto, si solo usas notificaciones de Android Auto.\nPara asegurarte de que la aplicación detecte la conexión de Android Auto, tienes que desactivar la optimización de batería.\nSi aún no funciona, también puedes habilitar el modo de primer plano o tienes que abrir la aplicación en Android Auto una vez, cuando esté conectado a Android Auto. + Configurar Android Auto + Android Auto es una aplicación independiente o parte del sistema y se puede acceder a ella a través de la configuración de Android.\nPara activar GlucoDataAuto para Android Auto, debes seguir los siguientes pasos: + 1. Activar el modo de desarrollador + - Abre Android Auto\n- Desplázate hacia abajo hasta la Versión\n- Toca varias veces en la Versión hasta que aparezca una ventana emergente que diga \"Permitir ajustes de desarollo\"\n- Presiona \"Aceptar\" + 2. Activar \"Fuentes desconocidas\" + - Abre Android Auto\n- Abre en el menú de 3 puntos las \"Ajustes de desarrollador\"\n- Desplázate hacia abajo hasta \"Fuentes desconocidas\"\n- Actívalo + 3. Configurar las configuraciones de notificaciones + - Abre Android Auto\n- Desplázate hacia abajo hasta \"Mensajes\"\n- Activa \"Mostrar notificaciones de mensajes\"\n- Activa \"Mostrar la primera línea de los mensajes\" + 4. Habilitar GlucoDataAuto + - Abre Android Auto\n- Desplázate hacia abajo hasta \"Pantalla\"\n- Abre \"Personalizar menú de apps\"\n- Habilita \"GlucoDataAuto\"\nSi GlucoDataAuto no está disponible, por favor reinicia el teléfono. + Diseño colorido + Muestra los valores en texto grande y colorido.\nEn algunos dispositivos Samsung con Android 13 y superior, esto puede hacer que la aplicación se bloquee debido a un error del lado de Samsung.\nSi ocurre el problema, esta configuración se desactivará automáticamente para garantizar que la aplicación siga siendo funcional. + ¡Error detectado! + ¡Lo siento mucho que la aplicación se haya bloqueado durante la ejecución!\nPor favor, envíame los registros para que pueda analizar el error e intentar solucionarlo lo antes posible.\n¡Gracias! \ No newline at end of file diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 00c089ee..93f7be91 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -476,4 +476,22 @@ Należy sprawdzić, czy strzałka trendu nie jest przycięta i czy nie jest zbyt duża. Interfejs użytkownika Ogólne ustawienia dotyczące interfejsu użytkownika tej aplikacji. + GlucoDataAuto działa + Włącz tę opcję tylko wtedy, gdy jako źródła używasz funkcji „follower” (obserwatora) i wyłącznie powiadomień z Android Auto.\nZapobiega to zamknięciu tej aplikacji przez system Android, gdy nie jest podłączona do Android Auto, więc aplikacja nie może rozpoznać połączenia z Android Auto.\nZamiast włączać to ustawienie, możesz również otworzyć aplikację w Android Auto raz, gdy jesteś już podłączony. + Optymalizacja baterii jest włączona!\nNaciśnij tutaj, aby ją wyłączyć. + Jeżeli jako źródła używasz funkcji „follower” (obserwatora), aplikacja może nie zostać uruchomiona podczas połączenia z Android Auto, gdy używa się wyłącznie powiadomień z Android Auto.\nAby upewnić się, że aplikacja wykrywa połączenie z Android Auto, należy wyłączyć optymalizację baterii.\nJeśli to nadal nie działa, można również włączyć tryb pierwszoplanowy lub otworzyć aplikację w Android Auto raz, gdy jest podłączona do Android Auto. + Konfiguracja Android Auto + Android Auto jest albo osobną aplikacją, albo częścią systemu i można do niej uzyskać dostęp poprzez ustawienia Androida.\nAby aktywować GlucoDataAuto dla Android Auto, należy wykonać następujące kroki: + 1. Aktywacja trybu programisty + - otwórz Android Auto\n- przewiń w dół do pozycji Wersja\n- dotknij kilkakrotnie pozycji Wersja, aż pojawi się wyskakujące okienko „Zezwalaj na ustawienia programisty\"\n- naciśnij \"OK\" + 2. Aktywuj „Nieznane źródła\" + - otwórz Android Auto\n- otwórz w menu (3 kropki) „Ustawienia programisty\"\n- przewiń w dół do pozycji „Nieznane źródła\"\n- włącz ją + 3. Ustawienia powiadomień + - otwórz Android Auto\n- przewiń w dół do pozycji „Powiadomienia\"\n- włącz „Pokaż podgląd wiadomości przychodzących\"\n- włącz „Pokazuj pierwszy wiersz wiadomości\" + 4. Włącz GlucoDataAuto + - otwórz Android Auto\n- przewiń w dół do pozycji \"Wyświetlanie\"\n- otwórz \"Dostosuj aplikacje w menu\"\n- włącz \"GlucoDataAuto\"\nJeśli GlucoDataAuto nie jest dostępny, proszę zrestartować telefon. + Kolorowy układ + Wyświetla wartości dużą i kolorową czcionką.\nNa niektórych urządzeniach Samsung z Androidem 13 i nowszym, może to spowodować awarię aplikacji z powodu błędu po stronie Samsunga.\nJeśli problem wystąpi, to ustawienie zostanie automatycznie wyłączone, aby aplikacja mogła działać poprawnie. + Wykryto awarię! + Bardzo mi przykro, że aplikacja zawiesiła się podczas działania!\nProszę, wyślij mi logi, abym mógł przeanalizować błąd i spróbować go jak najszybciej naprawić.\nDziękuję! diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index af896b46..b062503b 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -478,4 +478,22 @@ Você tem que verificar se o tendência está cortado, se fica muito grande. Interface de usuário Configurações gerais da interface do usuário deste aplicativo. + GlucoDataAuto está em execução + Ative isso apenas se você estiver usando uma fonte seguidora e somente notificações do Android Auto.\nIsso impede que o Android feche este aplicativo enquanto não estiver conectado ao Android Auto, para que o aplicativo não possa reconhecer a conexão com o Android Auto.\nEm vez de habilitar esta configuração, você também pode abrir o aplicativo no Android Auto uma vez, se estiver conectado. + Otimização da bateria está habilitada!\nPressione aqui para desativá-lo. + Se você estiver usando uma fonte seguidora, o aplicativo pode não ser iniciado enquanto estiver conectado ao Android Auto, se você estiver usando apenas notificações do Android Auto.\nPara garantir que o aplicativo detecte a conexão com o Android Auto, você deve desativar a otimização da bateria.\nSe ainda não funcionar, você também pode habilitar o modo de primeiro plano ou abrir o aplicativo no Android Auto uma vez, quando estiver conectado ao Android Auto. + Configurar Android Auto + O Android Auto é um aplicativo separado ou parte do sistema e pode ser acessado através das configurações do Android.\nPara ativar o GlucoDataAuto para Android Auto, você deve seguir os seguintes passos: + 1. Ativar o modo desenvolvedor + - Abra o Android Auto\n- Role para baixo até a Versão\n- Toque várias vezes na Versão até aparecer um pop-up para \"Permitir definições de programação\"\n- Pressione \"OK\" + 2. Ativar \"Fontes desconhecidas\" + - Abra o Android Auto\n- Abra no menu de 3 pontos as \"Definições do programador\"\n- Role para baixo até \"Fontes desconhecidas\"\n- Ative-o + 3. Configurar as configurações de notificações + - Abra o Android Auto\n- Role para baixo até \"Mensagens\"\n- Ative \"Mostrar notificações de mensagens\"\n- Ative \"Mostrar primeira linha das mensagens\" + 4. Habilitar GlucoDataAuto + - Abra o Android Auto\n- Role para baixo até \"Ecrã\"\n- Abra \"Personalize o Launcher\"\n- Habilite \"GlucoDataAuto\"\nSe o GlucoDataAuto não estiver disponível, reinicie o telefone, por favor. + Layout colorido + Exibe os valores em texto grande e colorido.\nEm alguns dispositivos Samsung com Android 13 e superior, isso pode fazer com que o aplicativo trave devido a um bug do lado da Samsung.\nSe o problema ocorrer, essa configuração será desativada automaticamente para garantir que o aplicativo permaneça funcional. + Falha detectada! + Sinto muito que o aplicativo tenha travado durante a execução!\nPor favor, envie-me os logs para que eu possa analisar o erro e tentar corrigi-lo o mais rápido possível.\nObrigado! diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index b8cedf9b..7c6f4fe2 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -493,4 +493,22 @@ Please disable this setting, if the trend arrow is to big and is cut off. User interface General settings for the user interface of this application. + GlucoDataAuto is running + Only enable it, if you are using a follower source and Android Auto notifications only.\nThis prevents Android from closing this app, while not connected to Android Auto, so the app can not recognize the Android Auto connection.\nInstead of enabling this setting, you can also open the app in Android Auto once, if you are connected. + Battery optimization is enabled!\nPress here to disable it. + If you are using a follower source, the app may not be started while connected to Android Auto, if you are using Android Auto Notifications only.\nTo make sure the app detects Android Auto connection you have to disable battery optimization.\nIf it still does not work, you can also enable foreground mode or you have to open the app in Android Auto once, if it is connected to Android Auto. + Setup Android Auto + Android Auto is either a separate app or part of the system and can be accessed through the Android settings.\nTo activate GlucoDataAuto for Android Auto, you have to do the following steps: + 1. Activate developer mode + - open Android Auto\n- scroll down to the Version\n- tap several times on the Version until a popup appears to \"Allow development settings\"\n- press \"OK\" + 2. Activate \"Unknown sources\" + - open Android Auto\n- open in the 3-dot menu the \"Developer settings\"\n- scroll down to \"Unknown source\"\n- enable it + 3. Set notification settings + - open Android Auto\n- scroll down to \"Messaging\"\n- enable \"Show message notifications\"\n- enable \"Show first line of messages\" + 4. Enable GlucoDataAuto + - open Android Auto\n- scroll down to \"Display\"\n- open \"Customise launcher\"\n- enable \"GlucoDataAuto\"\nIf GlucoDataAuto is not available, please restart your phone. + Colored layout + Displays the values in large and colored text.\nOn some Samsung devices with Android 13 and higher, this may cause the app to crash due to a bug on Samsung\'s side.\nIf the problem occurs, this setting will be automatically disabled to ensure the app remains functional. + Crash detected! + I am so sorry that the app crashed during execution!\nPlease send me the logs so I can analyze the error and try to fix it as soon as possible.\nThank you! diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index ac92e646..f2c66696 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -247,6 +247,7 @@ + requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) } + Dialogs.showOkDialog(this, CR.string.permission_notification_title, CR.string.permission_notification_message) { _, _ -> requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) } } else { this.requestPermissions(arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), 3) } @@ -384,7 +384,11 @@ class MainActivity : AppCompatActivity(), NotifierInterface { return true } R.id.action_snooze_30 -> { - AlarmHandler.setSnooze(30L) + if(BuildConfig.DEBUG) { + AlarmHandler.setSnooze(1L) + } else { + AlarmHandler.setSnooze(30L) + } return true } R.id.action_snooze_60 -> { @@ -444,7 +448,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { putBoolean(Constants.SHARED_PREF_ALARM_NOTIFICATION_ENABLED, false) apply() } - Dialogs.showOkDialog(this, resources.getString(CR.string.permission_alarm_notification_title), resources.getString(CR.string.permission_alarm_notification_message)) { _, _ -> + Dialogs.showOkDialog(this, CR.string.permission_alarm_notification_title, CR.string.permission_alarm_notification_message) { _, _ -> val intent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName) startActivity(intent) @@ -646,4 +650,17 @@ class MainActivity : AppCompatActivity(), NotifierInterface { const val CREATE_PHONE_FILE = 1 const val CREATE_WEAR_FILE = 2 } + + private fun checkUncaughtException() { + Log.d(LOG_ID, "Check uncaught exception ${sharedPref.getBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false)}") + if(sharedPref.getBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false)) { + val excMsg = sharedPref.getString(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_MESSAGE, "") + Log.e(LOG_ID, "Uncaught exception detected last time: $excMsg") + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_UNCAUGHT_EXCEPTION_DETECT, false) + apply() + } + Dialogs.showOkDialog(this, CR.string.app_crash_title, CR.string.app_crash_message, null) + } + } } \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/notification/PermanentNotification.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/notification/PermanentNotification.kt index dba4068b..aaf8d74a 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/notification/PermanentNotification.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/notification/PermanentNotification.kt @@ -80,6 +80,7 @@ object PermanentNotification: NotifierInterface, SharedPreferences.OnSharedPrefe } private fun createNofitication(context: Context) { + Log.d(LOG_ID, "createNofitication called") createNotificationChannel(context) Channels.getNotificationManager().cancel(GlucoDataService.NOTIFICATION_ID) @@ -108,7 +109,6 @@ object PermanentNotification: NotifierInterface, SharedPreferences.OnSharedPrefe .setVisibility(Notification.VISIBILITY_PUBLIC) } - private fun removeNotifications() { //notificationMgr.cancel(NOTIFICATION_ID) // remove notification showPrimaryNotification(false) @@ -188,57 +188,93 @@ object PermanentNotification: NotifierInterface, SharedPreferences.OnSharedPrefe return null } - fun getNotification(withContent: Boolean, iconKey: String, foreground: Boolean) : Notification { - var remoteViews: RemoteViews? = null - if (withContent) { - val bitmap = createNotificationView(GlucoDataService.context!!) - if(bitmap != null) { - remoteViews = RemoteViews(GlucoDataService.context!!.packageName, R.layout.image_view) - remoteViews.setImageViewBitmap(R.id.imageLayout, bitmap) - } - } - + fun getNotification(withContent: Boolean, iconKey: String, foreground: Boolean, customLayout: Boolean) : Notification { + Log.d(LOG_ID, "getNotification withContent=$withContent - foreground=$foreground - customLayout=$customLayout") val notificationBuilder = if(foreground) foregroundNotificationCompat else notificationCompat - val notification = notificationBuilder + val notificationBuild = notificationBuilder .setSmallIcon(getStatusBarIcon(iconKey)) .setContentIntent(getTapActionIntent(foreground)) .setWhen(ReceiveData.time) - .setCustomContentView(remoteViews) - .setCustomBigContentView(remoteViews) .setColorized(false) - .setStyle(Notification.DecoratedCustomViewStyle()) - .build() + + if (customLayout) { + Log.v(LOG_ID, "Use custom layout") + var remoteViews: RemoteViews? = null + if (withContent) { + val bitmap = createNotificationView(GlucoDataService.context!!) + if (bitmap != null) { + remoteViews = + RemoteViews(GlucoDataService.context!!.packageName, R.layout.image_view) + remoteViews.setImageViewBitmap(R.id.imageLayout, bitmap) + } + } + notificationBuild.setCustomContentView(remoteViews) + notificationBuild.setCustomBigContentView(remoteViews) + notificationBuild.setStyle(Notification.DecoratedCustomViewStyle()) + } else { + Log.v(LOG_ID, "Use default layout") + if (withContent) { + notificationBuild.setContentTitle(ReceiveData.getGlucoseAsString() + " Δ " + ReceiveData.getDeltaAsString()) + if (!ReceiveData.isIobCobObsolete(Constants.VALUE_OBSOLETE_LONG_SEC)) { + notificationBuild.setContentText("💉 " + ReceiveData.getIobAsString() + if (!ReceiveData.cob.isNaN()) (" " + "🍔 " + ReceiveData.getCobAsString()) else "") + } else { + notificationBuild.setContentText(null) + } + notificationBuild.setLargeIcon(BitmapUtils.getRateAsBitmap(withShadow = true)) + } else { + notificationBuild.setContentTitle(null) + notificationBuild.setContentText(null) + } + notificationBuild.setStyle(null) + } + val notification = notificationBuild.build() notification.visibility = Notification.VISIBILITY_PUBLIC notification.flags = notification.flags or Notification.FLAG_NO_CLEAR return notification } - private fun showNotification(id: Int, withContent: Boolean, iconKey: String, foreground: Boolean) { + private fun showNotification(id: Int, withContent: Boolean, iconKey: String, foreground: Boolean, customLayout: Boolean) { try { Log.v(LOG_ID, "showNotification called for id " + id) Channels.getNotificationManager().notify( id, - getNotification(withContent, iconKey, foreground) + getNotification(withContent, iconKey, foreground, customLayout) ) } catch (exc: Exception) { Log.e(LOG_ID, "showNotification exception: " + exc.toString() ) } } - fun showNotifications() { - showPrimaryNotification(true) - if (sharedPref.getBoolean(Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION, false)) { - Log.d(LOG_ID, "show second notification") - showNotification(SECOND_NOTIFICATION_ID, false, Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION_ICON, false) - } else { - Channels.getNotificationManager().cancel(SECOND_NOTIFICATION_ID) + fun showNotifications(onlySecond: Boolean = false) { + Log.d(LOG_ID, "showNotifications service running: ${GlucoDataService.foreground} - onlySecond=$onlySecond") + if (GlucoDataService.foreground) { + if (!onlySecond) + showPrimaryNotification(true) + if (sharedPref.getBoolean(Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION, false)) { + Log.d(LOG_ID, "show second notification") + showNotification( + SECOND_NOTIFICATION_ID, + false, + Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION_ICON, + false, + false + ) + } else { + Channels.getNotificationManager().cancel(SECOND_NOTIFICATION_ID) + } } } private fun showPrimaryNotification(show: Boolean) { Log.d(LOG_ID, "showPrimaryNotification " + show) - showNotification(GlucoDataService.NOTIFICATION_ID, !sharedPref.getBoolean(Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY, false), Constants.SHARED_PREF_PERMANENT_NOTIFICATION_ICON, true) + showNotification( + GlucoDataService.NOTIFICATION_ID, + !sharedPref.getBoolean(Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY, false), + Constants.SHARED_PREF_PERMANENT_NOTIFICATION_ICON, + true, + sharedPref.getBoolean(Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT, true) + ) } private fun hasContent(): Boolean { @@ -287,7 +323,7 @@ object PermanentNotification: NotifierInterface, SharedPreferences.OnSharedPrefe } } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { try { Log.d(LOG_ID, "onSharedPreferenceChanged called for key " + key) when(key) { @@ -298,8 +334,16 @@ object PermanentNotification: NotifierInterface, SharedPreferences.OnSharedPrefe Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION_TAP_ACTION, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_USE_BIG_ICON, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_COLORED_ICON, - Constants.SHARED_PREF_PERMANENT_NOTIFICATION_TAP_ACTION, + Constants.SHARED_PREF_PERMANENT_NOTIFICATION_TAP_ACTION -> { + updatePreferences() + } Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY -> { + if(!sharedPreferences.getBoolean(Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT, true)) + createNofitication(GlucoDataService.context!!) // reset notification to remove large icon + updatePreferences() + } + Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT-> { + createNofitication(GlucoDataService.context!!) // reset notification to remove large icon updatePreferences() } } diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/AlarmFragment.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/AlarmFragment.kt index e7b4d828..933e7630 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/AlarmFragment.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/AlarmFragment.kt @@ -188,7 +188,7 @@ class AlarmFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPref } } private fun requestChannelActivation() { - Dialogs.showOkDialog(requireContext(), resources.getString(CR.string.permission_alarm_notification_title), resources.getString(CR.string.permission_alarm_notification_message)) { _, _ -> + Dialogs.showOkDialog(requireContext(),CR.string.permission_alarm_notification_title, CR.string.permission_alarm_notification_message) { _, _ -> val intent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) startActivity(intent) diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SettingsFragmentBase.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SettingsFragmentBase.kt index bb5dd184..028d2681 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SettingsFragmentBase.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SettingsFragmentBase.kt @@ -152,10 +152,10 @@ abstract class SettingsFragmentBase(private val prefResId: Int) : PreferenceFrag } } - fun setEnableState(sharedPreferences: SharedPreferences, key: String, enableKey: String, secondEnableKey: String? = null, defValue: Boolean = false) { + fun setEnableState(sharedPreferences: SharedPreferences, key: String, enableKey: String, secondEnableKey: String? = null, defValue: Boolean = false, invert: Boolean = false) { val pref = findPreference(key) if (pref != null) { - pref.isEnabled = sharedPreferences.getBoolean(enableKey, defValue) && (if (secondEnableKey != null) sharedPreferences.getBoolean(secondEnableKey, defValue) else true) + pref.isEnabled = invert != (sharedPreferences.getBoolean(enableKey, defValue) && (if (secondEnableKey != null) sharedPreferences.getBoolean(secondEnableKey, defValue) else true)) if(!updateEnablePrefs.contains(enableKey)) { Log.v(LOG_ID, "Add update enable pref $enableKey") @@ -191,6 +191,7 @@ abstract class SettingsFragmentBase(private val prefResId: Int) : PreferenceFrag setEnableState(sharedPreferences, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY, Constants.SHARED_PREF_PERMANENT_NOTIFICATION, defValue = true) setEnableState(sharedPreferences, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_USE_BIG_ICON, Constants.SHARED_PREF_PERMANENT_NOTIFICATION, defValue = true) setEnableState(sharedPreferences, Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION, Constants.SHARED_PREF_PERMANENT_NOTIFICATION, defValue = true)*/ + setEnableState(sharedPreferences, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_CUSTOM_LAYOUT, Constants.SHARED_PREF_PERMANENT_NOTIFICATION_EMPTY, defValue = true, invert = true) setEnableState(sharedPreferences, Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION_ICON, /*Constants.SHARED_PREF_PERMANENT_NOTIFICATION,*/Constants.SHARED_PREF_SECOND_PERMANENT_NOTIFICATION, defValue = false) setEnableState(sharedPreferences, Constants.SHARED_PREF_FLOATING_WIDGET_SIZE, Constants.SHARED_PREF_FLOATING_WIDGET) setEnableState(sharedPreferences, Constants.SHARED_PREF_FLOATING_WIDGET_STYLE, Constants.SHARED_PREF_FLOATING_WIDGET) @@ -241,7 +242,29 @@ abstract class SettingsFragmentBase(private val prefResId: Int) : PreferenceFrag class GeneralSettingsFragment: SettingsFragmentBase(R.xml.pref_general) {} class RangeSettingsFragment: SettingsFragmentBase(R.xml.pref_target_range) {} class UiSettingsFragment: SettingsFragmentBase(R.xml.pref_ui) {} -class WidgetSettingsFragment: SettingsFragmentBase(R.xml.pref_widgets) {} +class WidgetSettingsFragment: SettingsFragmentBase(R.xml.pref_widgets) { + + override fun initPreferences() { + Log.v(LOG_ID, "initPreferences called") + super.initPreferences() + updateStyleSummary() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + super.onSharedPreferenceChanged(sharedPreferences, key) + when (key) { + Constants.SHARED_PREF_FLOATING_WIDGET_STYLE -> updateStyleSummary() + } + } + + private fun updateStyleSummary() { + val stylePref = findPreference(Constants.SHARED_PREF_FLOATING_WIDGET_STYLE) + if(stylePref != null) { + stylePref.summary = stylePref.entry + } + } + +} class NotificaitonSettingsFragment: SettingsFragmentBase(R.xml.pref_notification) {} class LockscreenSettingsFragment: SettingsFragmentBase(R.xml.pref_lockscreen) {} class WatchSettingsFragment: SettingsFragmentBase(R.xml.pref_watch) { @@ -267,6 +290,7 @@ class WatchSettingsFragment: SettingsFragmentBase(R.xml.pref_watch) { class TransferSettingsFragment: SettingsFragmentBase(R.xml.pref_transfer) { override fun initPreferences() { Log.v(LOG_ID, "initPreferences called") + super.initPreferences() setupReceivers(Constants.GLUCODATA_BROADCAST_ACTION, Constants.SHARED_PREF_GLUCODATA_RECEIVERS) setupReceivers(Constants.XDRIP_ACTION_GLUCOSE_READING, Constants.SHARED_PREF_XDRIP_RECEIVERS) setupReceivers(Constants.XDRIP_BROADCAST_ACTION, Constants.SHARED_PREF_XDRIP_BROADCAST_RECEIVERS) @@ -276,6 +300,7 @@ class TransferSettingsFragment: SettingsFragmentBase(R.xml.pref_transfer) { class GDASettingsFragment: SettingsFragmentBase(R.xml.pref_gda) { override fun initPreferences() { Log.v(LOG_ID, "initPreferences called") + super.initPreferences() if (PackageUtils.isGlucoDataAutoAvailable(requireContext())) { val sendToGDA = findPreference(Constants.SHARED_PREF_SEND_TO_GLUCODATAAUTO) sendToGDA!!.isVisible = true diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SourceFragment.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SourceFragment.kt index 493d8414..215d55df 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SourceFragment.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/preferences/SourceFragment.kt @@ -134,7 +134,7 @@ class SourceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPre val pref = findPreference(key) if(pref != null) { val value = preferenceManager.sharedPreferences!!.getString(key, "")!!.trim() - pref.summary = if(value.isNullOrEmpty()) + pref.summary = if(value.isEmpty()) resources.getString(defaultResId) else value @@ -156,7 +156,7 @@ class SourceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPre Constants.SHARED_PREF_LIBRE_PATIENT_ID, "" )!!.trim() - if (value.isNullOrEmpty() || !LibreLinkSourceTask.patientData.containsKey(value)) + if (value.isEmpty() || !LibreLinkSourceTask.patientData.containsKey(value)) pref.summary = resources.getString(CR.string.src_libre_patient_summary) else { pref.summary = LibreLinkSourceTask.patientData[value] @@ -181,8 +181,8 @@ class SourceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPre private fun setupLocalIobAction(preference: Preference?) { if(preference != null) { preference.setOnPreferenceClickListener { - Dialogs.showOkCancelDialog(requireContext(), resources.getString(de.michelinside.glucodatahandler.common.R.string.activate_local_nightscout_iob_title), resources.getString( - de.michelinside.glucodatahandler.common.R.string.activate_local_nightscout_iob_message)) { _, _ -> + Dialogs.showOkCancelDialog(requireContext(), CR.string.activate_local_nightscout_iob_title, + CR.string.activate_local_nightscout_iob_message) { _, _ -> with(preferenceManager!!.sharedPreferences!!.edit()) { putBoolean(Constants.SHARED_PREF_NIGHTSCOUT_IOB_COB, true) putString(Constants.SHARED_PREF_NIGHTSCOUT_URL, "http://127.0.0.1:17580") diff --git a/mobile/src/main/res/xml/pref_notification.xml b/mobile/src/main/res/xml/pref_notification.xml index 1ac46a04..c8a81710 100644 --- a/mobile/src/main/res/xml/pref_notification.xml +++ b/mobile/src/main/res/xml/pref_notification.xml @@ -24,6 +24,12 @@ android:summary="@string/pref_notification_icon_summary" android:title="@string/pref_notification_icon" app:iconSpaceReserved="false" /> + +