diff --git a/auto/build.gradle b/auto/build.gradle index ca1c44c2..5b510e64 100644 --- a/auto/build.gradle +++ b/auto/build.gradle @@ -11,8 +11,8 @@ android { applicationId "de.michelinside.glucodataauto" minSdk rootProject.minSdk targetSdk rootProject.targetSdk - versionCode 1000 + rootProject.versionCode - versionName rootProject.versionName + versionCode 1022 + versionName "0.9.9.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -29,7 +29,6 @@ android { resValue "string", "app_name", "GlucoDataAuto" } debug { - applicationIdSuffix '.debug' minifyEnabled false resValue "string", "app_name", "GlucoDataAuto Debug" } @@ -59,6 +58,12 @@ android { buildFeatures { viewBinding true } + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } } dependencies { @@ -72,6 +77,7 @@ dependencies { implementation "androidx.preference:preference:1.2.1" implementation "com.jaredrummler:colorpicker:1.1.0" implementation "androidx.media:media:1.7.0" + implementation 'androidx.work:work-runtime:2.9.0' } afterEvaluate { diff --git a/auto/src/main/AndroidManifest.xml b/auto/src/main/AndroidManifest.xml index e74cd133..46b4c8b6 100644 --- a/auto/src/main/AndroidManifest.xml +++ b/auto/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + @@ -54,6 +56,17 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity" /> + + + + + + + + + + "Not connected to a head unit" + CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS" + CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto" + else -> "Unknown car connection type" + } + Log.v(LOG_ID, "onConnectionStateUpdated: " + message + " (" + connectionState.toString() + ")") + if (init) { + if (connectionState == CarConnection.CONNECTION_TYPE_NOT_CONNECTED) { + if(car_connected) { + Log.i(LOG_ID, "Exited Car Mode") + car_connected = false + stop(GlucoDataService.context!!) + } + } else { + if(!car_connected) { + Log.i(LOG_ID, "Entered Car Mode") + car_connected = true + start(GlucoDataService.context!!) + } + } + InternalNotifier.notify(GlucoDataService.context!!, NotifySource.CAR_CONNECTION, null) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onConnectionStateUpdated exception: " + exc.message.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun sendStateBroadcast(context: Context, enabled: Boolean) { + try { + Log.d(LOG_ID, "Sending state broadcast for state: " + enabled) + val intent = Intent(Constants.GLUCODATAAUTO_STATE_ACTION) + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + intent.putExtra(Constants.GLUCODATAAUTO_STATE_EXTRA, enabled) + context.sendBroadcast(intent) + } catch (exc: Exception) { + Log.e(LOG_ID, "sendStateBroadcast exception: " + exc.toString()) + } + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + try { + Log.d(LOG_ID, "onStartCommand called") + super.onStartCommand(intent, flags, startId) + val isForeground = intent.getBooleanExtra(Constants.SHARED_PREF_FOREGROUND_SERVICE, false) + if (isForeground && !isForegroundService && Utils.checkPermission(this, android.Manifest.permission.POST_NOTIFICATIONS, Build.VERSION_CODES.TIRAMISU)) { + Log.i(LOG_ID, "Starting service in foreground!") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + startForeground(NOTIFICATION_ID, getNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + else + startForeground(NOTIFICATION_ID, getNotification()) + isForegroundService = true + } else if ( isForegroundService && !isForeground ) { + isForegroundService = false + Log.i(LOG_ID, "Stopping service in foreground!") + stopForeground(STOP_FOREGROUND_REMOVE) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onStartCommand exception: " + exc.toString()) + } + if (isForegroundService) + return START_STICKY // keep alive + return START_NOT_STICKY + } + + override fun onDestroy() { + Log.v(LOG_ID, "onDestroy called") + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? { + Log.v(LOG_ID, "onBind called with intent " + intent) + return null + } + + private fun getNotification(): Notification { + Channels.createNotificationChannel(this, ChannelType.ANDROID_AUTO_FOREGROUND) + + val pendingIntent = Utils.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)) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setAutoCancel(false) + .setCategory(Notification.CATEGORY_STATUS) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .build() + } + +} \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt b/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt index 937ac13b..f7cfb66f 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/MainActivity.kt @@ -1,5 +1,6 @@ package de.michelinside.glucodataauto +import android.app.Activity import android.app.AlarmManager import android.content.Context import android.content.Intent @@ -19,7 +20,6 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuCompat import androidx.preference.PreferenceManager -import de.michelinside.glucodataauto.android_auto.CarNotification import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.notifier.InternalNotifier @@ -27,6 +27,9 @@ import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.BitmapUtils import de.michelinside.glucodatahandler.common.utils.Utils +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import de.michelinside.glucodatahandler.common.R as CR class MainActivity : AppCompatActivity(), NotifierInterface { @@ -81,23 +84,18 @@ class MainActivity : AppCompatActivity(), NotifierInterface { apply() } } - CarNotification.initNotification(this) + GlucoDataServiceAuto.init(this) requestPermission() } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) } } - override fun onDestroy() { - super.onDestroy() - if (!CarNotification.connected) - CarNotification.cleanupNotification(this) - } - override fun onPause() { try { super.onPause() InternalNotifier.remNotifier(this, this) + GlucoDataServiceAuto.stopDataSync(this) Log.v(LOG_ID, "onPause called") } catch (exc: Exception) { Log.e(LOG_ID, "onPause exception: " + exc.message.toString() ) @@ -125,6 +123,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { Log.i(LOG_ID, "Notification permission granted") requestNotificationPermission = false } + GlucoDataServiceAuto.startDataSync(this) } catch (exc: Exception) { Log.e(LOG_ID, "onResume exception: " + exc.message.toString() ) } @@ -214,6 +213,10 @@ class MainActivity : AppCompatActivity(), NotifierInterface { startActivity(mailIntent) return true } + R.id.action_save_mobile_logs -> { + SaveMobileLogs() + return true + } else -> return super.onOptionsItemSelected(item) } } catch (exc: Exception) { @@ -234,7 +237,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { } viewIcon.setImageIcon(BitmapUtils.getRateAsIcon()) txtLastValue.text = ReceiveData.getAsString(this, CR.string.gda_no_data) - txtCarInfo.text = if (CarNotification.connected) resources.getText(CR.string.activity_main_car_connected_label) else resources.getText(CR.string.activity_main_car_disconnected_label) + txtCarInfo.text = if (GlucoDataServiceAuto.connected) resources.getText(CR.string.activity_main_car_connected_label) else resources.getText(CR.string.activity_main_car_disconnected_label) } catch (exc: Exception) { Log.e(LOG_ID, "update exception: " + exc.message.toString() ) } @@ -244,4 +247,43 @@ class MainActivity : AppCompatActivity(), NotifierInterface { Log.v(LOG_ID, "new intent received") update() } + + private fun SaveMobileLogs() { + try { + Log.v(LOG_ID, "Save mobile logs called") + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + val currentDateandTime = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format( + Date() + ) + val fileName = "GDA_" + currentDateandTime + ".txt" + putExtra(Intent.EXTRA_TITLE, fileName) + } + startActivityForResult(intent, CREATE_FILE) + + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving mobile logs exception: " + exc.message.toString() ) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + try { + Log.v(LOG_ID, "onActivityResult called for requestCode: " + requestCode + " - resultCode: " + resultCode + " - data: " + Utils.dumpBundle(data?.extras)) + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + if (requestCode == CREATE_FILE) { + data?.data?.also { uri -> + Utils.saveLogs(this, uri) + } + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving logs exception: " + exc.message.toString() ) + } + } + + companion object { + const val CREATE_FILE = 1 + } } \ 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 14207f7b..f49cba84 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 @@ -13,14 +13,13 @@ import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log 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.utils.BitmapUtils import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource -import de.michelinside.glucodatahandler.common.tasks.BackgroundWorker -import de.michelinside.glucodatahandler.common.tasks.TimeTaskService class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, SharedPreferences.OnSharedPreferenceChangeListener { private val LOG_ID = "GDH.AA.CarMediaBrowserService" @@ -29,13 +28,17 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh private lateinit var sharedPref: SharedPreferences private lateinit var session: MediaSessionCompat + companion object { + var active = false + } + override fun onCreate() { Log.v(LOG_ID, "onCreate") try { super.onCreate() - TimeTaskService.useWorker = true - CarNotification.initNotification(this) - ReceiveData.initData(this) + active = true + GlucoDataServiceAuto.init(this) + GlucoDataServiceAuto.start(this) sharedPref = this.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) sharedPref.registerOnSharedPreferenceChangeListener(this) @@ -69,11 +72,11 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh override fun onDestroy() { Log.v(LOG_ID, "onDestroy") try { + active = false InternalNotifier.remNotifier(this, this) sharedPref.unregisterOnSharedPreferenceChangeListener(this) session.release() - CarNotification.cleanupNotification(this) - BackgroundWorker.stopAllWork(this) + GlucoDataServiceAuto.stop(this) super.onDestroy() } catch (exc: Exception) { Log.e(LOG_ID, "onDestroy exception: " + exc.message.toString() ) @@ -123,7 +126,8 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh Log.v(LOG_ID, "onSharedPreferenceChanged called for key " + key) try { when(key) { - Constants.SHARED_PREF_CAR_MEDIA -> { + Constants.SHARED_PREF_CAR_MEDIA, + Constants.SHARED_PREF_CAR_MEDIA_ICON_STYLE -> { notifyChildrenChanged(MEDIA_ROOT_ID) } } @@ -133,12 +137,18 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh } private fun getIcon(size: Int = 100): Bitmap? { - return BitmapUtils.textRateToBitmap(ReceiveData.getClucoseAsString(), ReceiveData.rate, ReceiveData.getClucoseColor(), ReceiveData.isObsolete( - Constants.VALUE_OBSOLETE_SHORT_SEC), ReceiveData.isObsolete(Constants.VALUE_OBSOLETE_SHORT_SEC) && !ReceiveData.isObsolete(), - size, size) + return when(sharedPref.getString(Constants.SHARED_PREF_CAR_MEDIA_ICON_STYLE, Constants.AA_MEDIA_ICON_STYLE_GLUCOSE_TREND)) { + Constants.AA_MEDIA_ICON_STYLE_TREND -> { + BitmapUtils.getRateAsBitmap(width = size, height = size) + } + else -> { + BitmapUtils.getGlucoseTrendBitmap(width = size, height = size) + } + } } private fun createMediaItem(): MediaBrowserCompat.MediaItem { + Log.v(LOG_ID, "createMediaItem called") if (sharedPref.getBoolean(Constants.SHARED_PREF_CAR_MEDIA,true)) { session.setPlaybackState(buildState(PlaybackState.STATE_PAUSED)) session.setMetadata( @@ -159,7 +169,7 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh } val mediaDescriptionBuilder = MediaDescriptionCompat.Builder() .setMediaId(MEDIA_GLUCOSE_ID) - .setTitle("Delta: " + ReceiveData.getDeltaAsString() + "\n" + ReceiveData.getElapsedTimeMinuteAsString(this)) + .setTitle(ReceiveData.getClucoseAsString() + " (Δ " + ReceiveData.getDeltaAsString() + ")\n" + ReceiveData.getElapsedTimeMinuteAsString(this)) //.setSubtitle(ReceiveData.timeformat.format(Date(ReceiveData.time))) .setIconBitmap(getIcon()!!) return MediaBrowserCompat.MediaItem( @@ -167,6 +177,7 @@ class CarMediaBrowserService: MediaBrowserServiceCompat(), NotifierInterface, Sh } private fun buildState(state: Int): PlaybackStateCompat? { + Log.v(LOG_ID, "buildState called for state " + state) return PlaybackStateCompat.Builder() .setState( state, 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 5f354a24..eca2a026 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 @@ -7,7 +7,6 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.util.Log -import androidx.car.app.connection.CarConnection import androidx.car.app.notification.CarAppExtender import androidx.car.app.notification.CarNotificationManager import androidx.core.app.NotificationChannelCompat @@ -15,12 +14,14 @@ import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.graphics.drawable.IconCompat +import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodataauto.R import de.michelinside.glucodatahandler.common.R as CR import de.michelinside.glucodatahandler.common.* import de.michelinside.glucodatahandler.common.notifier.* import de.michelinside.glucodatahandler.common.utils.BitmapUtils import de.michelinside.glucodatahandler.common.notification.ChannelType +import de.michelinside.glucodatahandler.common.tasks.ElapsedTimeTask import de.michelinside.glucodatahandler.common.utils.Utils import java.text.DateFormat import java.util.* @@ -35,17 +36,14 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC @SuppressLint("StaticFieldLeak") private lateinit var notificationMgr: CarNotificationManager private var show_notification = false // default: no notification - private var car_connected = false private var notification_interval = 1L // every minute -> always, -1L: only for alarms + private var notification_reappear_interval = 5L const val LAST_NOTIFCATION_TIME = "last_notification_time" private var last_notification_time = 0L const val FORCE_NEXT_NOTIFY = "force_next_notify" private var forceNextNotify = false - val connected: Boolean get() = car_connected @SuppressLint("StaticFieldLeak") private lateinit var notificationCompat: NotificationCompat.Builder - @SuppressLint("StaticFieldLeak") - private var context: Context? = null var enable_notification : Boolean get() { return show_notification @@ -89,27 +87,34 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC enable_notification = sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION, enable_notification) val alarmOnly = sharedPref.getBoolean(Constants.SHARED_PREF_CAR_NOTIFICATION_ALARM_ONLY, true) notification_interval = if (alarmOnly) -1 else sharedPref.getInt(Constants.SHARED_PREF_CAR_NOTIFICATION_INTERVAL_NUM, 1).toLong() - Log.i(LOG_ID, "notification settings changed: active: " + enable_notification + " - interval: " + notification_interval) - if(init && car_connected && cur_enabled != enable_notification) { - if(enable_notification) - showNotification(context!!, false) - else - removeNotification() + val reappear_active = notification_reappear_interval > 0 + notification_reappear_interval = if (alarmOnly) 0L else sharedPref.getInt(Constants.SHARED_PREF_CAR_NOTIFICATION_REAPPEAR_INTERVAL, 5).toLong() + Log.i(LOG_ID, "notification settings changed: active: " + enable_notification + " - interval: " + notification_interval + " - reappear:" + notification_reappear_interval) + if(init && GlucoDataServiceAuto.connected) { + if (enable_notification) + ElapsedTimeTask.setInterval(notification_reappear_interval) + if (cur_enabled != enable_notification) { + if (enable_notification) { + showNotification(GlucoDataService.context!!, NotifySource.BROADCAST) + } else + removeNotification() + } else if (reappear_active != (notification_reappear_interval > 0) && InternalNotifier.hasNotifier(this) ) { + Log.d(LOG_ID, "Update internal notification filter for reappear interval: " + notification_reappear_interval) + InternalNotifier.addNotifier(GlucoDataService.context!!, this, getFilter()) + } } } - fun initNotification(context: Context) { + private fun initNotification(context: Context) { try { if(!init) { Log.v(LOG_ID, "initNotification called") - CarNotification.context = context val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) migrateSettings(sharedPref) sharedPref.registerOnSharedPreferenceChangeListener(this) updateSettings(sharedPref) loadExtras(context) createNofitication(context) - CarConnection(context.applicationContext).type.observeForever(CarNotification::onConnectionStateUpdated) init = true } } catch (exc: Exception) { @@ -132,57 +137,49 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC } } - fun cleanupNotification(context: Context) { + private fun cleanupNotification(context: Context) { try { if (init) { - Log.v(LOG_ID, "remNotification called") - CarConnection(context.applicationContext).type.removeObserver(CarNotification::onConnectionStateUpdated) + Log.v(LOG_ID, "cleanupNotification called") val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) sharedPref.unregisterOnSharedPreferenceChangeListener(this) init = false - CarNotification.context = null } } catch (exc: Exception) { - Log.e(LOG_ID, "init exception: " + exc.message.toString()) + Log.e(LOG_ID, "cleanupNotification exception: " + exc.message.toString()) } } - fun onConnectionStateUpdated(connectionState: Int) { - try { - val message = when(connectionState) { - CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit" - CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS" - CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto" - else -> "Unknown car connection type" - } - Log.v(LOG_ID, "onConnectionStateUpdated: " + message + " (" + connectionState.toString() + ")") - if (init) { - if (connectionState == CarConnection.CONNECTION_TYPE_NOT_CONNECTED) { - Log.i(LOG_ID, "Exited Car Mode") - removeNotification() - car_connected = false - InternalNotifier.remNotifier(context!!, this) - } else { - Log.i(LOG_ID, "Entered Car Mode") - forceNextNotify = false - car_connected = true - InternalNotifier.addNotifier(context!!, this, mutableSetOf( - NotifySource.BROADCAST, - NotifySource.MESSAGECLIENT, - NotifySource.OBSOLETE_VALUE)) - showNotification(context!!, false) - } - InternalNotifier.notify(context!!, NotifySource.CAR_CONNECTION, null) - } - } catch (exc: Exception) { - Log.e(LOG_ID, "onConnectionStateUpdated exception: " + exc.message.toString() + "\n" + exc.stackTraceToString() ) - } + private fun getFilter(): MutableSet { + val filter = mutableSetOf( + NotifySource.BROADCAST, + NotifySource.MESSAGECLIENT) + if (notification_reappear_interval > 0) + filter.add(NotifySource.TIME_VALUE) + return filter + } + + fun enable(context: Context) { + Log.d(LOG_ID, "enable called") + initNotification(context) + forceNextNotify = false + InternalNotifier.addNotifier(GlucoDataService.context!!, this, getFilter()) + showNotification(context, NotifySource.BROADCAST) + if (enable_notification) + ElapsedTimeTask.setInterval(notification_reappear_interval) + } + + fun disable(context: Context) { + Log.d(LOG_ID, "disable called") + removeNotification() + InternalNotifier.remNotifier(context, this) + cleanupNotification(context) } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { Log.v(LOG_ID, "OnNotifyData called for source " + dataSource) try { - showNotification(context, dataSource == NotifySource.OBSOLETE_VALUE) + showNotification(context, dataSource) } catch (exc: Exception) { Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + "\n" + exc.stackTraceToString() ) } @@ -191,43 +188,68 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC fun removeNotification() { notificationMgr.cancel(NOTIFICATION_ID) // remove notification forceNextNotify = false + ElapsedTimeTask.setInterval(0L) } private fun getTimeDiffMinute(): Long { return Utils.round((ReceiveData.time-last_notification_time).toFloat()/60000, 0).toLong() } - private fun canShowNotification(isObsolete: Boolean): Boolean { - if (init && enable_notification && car_connected) { - if(notification_interval == 1L || ReceiveData.forceAlarm) - return true - if (ReceiveData.getAlarmType() == ReceiveData.AlarmType.VERY_LOW || isObsolete) { - forceNextNotify = true // if obsolete or VERY_LOW, the next value is important! - return true - } - if (forceNextNotify) { - forceNextNotify = false - return true - } - if (notification_interval > 1L) { - return getTimeDiffMinute() >= notification_interval + private fun canShowNotification(dataSource: NotifySource): Boolean { + if (init && enable_notification && GlucoDataServiceAuto.connected) { + Log.d(LOG_ID, "Check showing notificiation:" + + "\ndataSource: " + dataSource + + "\nalarm-type: " + ReceiveData.getAlarmType() + + "\nforceNextNotify: " + forceNextNotify + + "\nnotify-elapsed: " + getTimeDiffMinute() + + "\ndata-elapsed: " + ReceiveData.getElapsedTimeMinute() + + "\nnotification_interval: " + notification_interval + + "\nnotification_reappear_interval: " + notification_reappear_interval + ) + if (dataSource == NotifySource.BROADCAST || dataSource == NotifySource.MESSAGECLIENT) { + if(notification_interval == 1L || ReceiveData.forceAlarm) { + Log.v(LOG_ID, "Notification has forced by interval or alarm") + return true + } + if (ReceiveData.getAlarmType() == ReceiveData.AlarmType.VERY_LOW) { + Log.v(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") + forceNextNotify = false + return true + } + if (notification_interval > 1L && getTimeDiffMinute() >= notification_interval && ReceiveData.getElapsedTimeMinute() == 0L) { + Log.v(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) + return true + } } + Log.v(LOG_ID, "No notification to show") return false } return false } - fun showNotification(context: Context, isObsolete: Boolean) { + fun showNotification(context: Context, dataSource: NotifySource) { try { - if (canShowNotification(isObsolete)) { + if (canShowNotification(dataSource)) { Log.v(LOG_ID, "showNotification called") notificationCompat .setLargeIcon(BitmapUtils.getRateAsBitmap(resizeFactor = 0.75F)) .setWhen(ReceiveData.time) - .setStyle(createMessageStyle(context, isObsolete)) + .setStyle(createMessageStyle(context, ReceiveData.isObsolete(Constants.VALUE_OBSOLETE_SHORT_SEC))) notificationMgr.notify(NOTIFICATION_ID, notificationCompat) - last_notification_time = ReceiveData.time - saveExtras(context) + if(dataSource != NotifySource.TIME_VALUE) { + last_notification_time = ReceiveData.time + saveExtras(context) + } } } catch (exc: Exception) { Log.e(LOG_ID, "showNotification exception: " + exc.toString() ) @@ -293,7 +315,8 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC when(key) { Constants.SHARED_PREF_CAR_NOTIFICATION, Constants.SHARED_PREF_CAR_NOTIFICATION_ALARM_ONLY, - Constants.SHARED_PREF_CAR_NOTIFICATION_INTERVAL_NUM -> { + Constants.SHARED_PREF_CAR_NOTIFICATION_INTERVAL_NUM, + Constants.SHARED_PREF_CAR_NOTIFICATION_REAPPEAR_INTERVAL -> { updateSettings(sharedPreferences!!) } } @@ -327,7 +350,7 @@ object CarNotification: NotifierInterface, SharedPreferences.OnSharedPreferenceC forceNextNotify = sharedAutoPref.getBoolean(FORCE_NEXT_NOTIFY, forceNextNotify) } } catch (exc: Exception) { - Log.e(LOG_ID, "Saving extras exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + Log.e(LOG_ID, "Loading extras exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) } } 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 3a0bdf41..596d72dc 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/preferences/SettingsFragment.kt @@ -73,6 +73,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP try { setEnableState(sharedPreferences, Constants.SHARED_PREF_CAR_NOTIFICATION_ALARM_ONLY, Constants.SHARED_PREF_CAR_NOTIFICATION) setEnableState(sharedPreferences, Constants.SHARED_PREF_CAR_NOTIFICATION_INTERVAL_NUM, Constants.SHARED_PREF_CAR_NOTIFICATION, Constants.SHARED_PREF_CAR_NOTIFICATION_ALARM_ONLY) + setEnableState(sharedPreferences, Constants.SHARED_PREF_CAR_NOTIFICATION_REAPPEAR_INTERVAL, Constants.SHARED_PREF_CAR_NOTIFICATION, Constants.SHARED_PREF_CAR_NOTIFICATION_ALARM_ONLY) } catch (exc: Exception) { Log.e(LOG_ID, "updateEnableStates exception: " + exc.toString()) } diff --git a/auto/src/main/java/de/michelinside/glucodataauto/receiver/AAPSReceiver.kt b/auto/src/main/java/de/michelinside/glucodataauto/receiver/AAPSReceiver.kt new file mode 100644 index 00000000..a220fc51 --- /dev/null +++ b/auto/src/main/java/de/michelinside/glucodataauto/receiver/AAPSReceiver.kt @@ -0,0 +1,20 @@ +package de.michelinside.glucodataauto.receiver + +import android.content.Context +import android.content.Intent +import android.util.Log +import de.michelinside.glucodataauto.GlucoDataServiceAuto +import de.michelinside.glucodatahandler.common.receiver.AAPSReceiver as BaseAAPSReceiver + +class AAPSReceiver : BaseAAPSReceiver() { + private val LOG_ID = "GDH.AA.AAPSReceiver" + override fun onReceive(context: Context, intent: Intent) { + try { + Log.v(LOG_ID, intent.action + " receveived: " + intent.extras.toString()) + GlucoDataServiceAuto.init(context) + super.onReceive(context, intent) + } catch (exc: Exception) { + Log.e(LOG_ID, "Receive exception: " + exc.message.toString()) + } + } +} \ No newline at end of file diff --git a/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataActionReceiver.kt b/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataActionReceiver.kt index fb8999f4..1998bc5d 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataActionReceiver.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataActionReceiver.kt @@ -4,7 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log -import de.michelinside.glucodataauto.android_auto.CarNotification +import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.notifier.DataSource @@ -13,7 +13,7 @@ open class GlucoDataActionReceiver: BroadcastReceiver() { private val LOG_ID = "GDH.AA.GlucoDataActionReceiver" override fun onReceive(context: Context, intent: Intent) { try { - CarNotification.initNotification(context) + GlucoDataServiceAuto.init(context) val action = intent.action Log.v(LOG_ID, intent.action + " receveived: " + intent.extras.toString()) if (action != Constants.GLUCODATA_ACTION) { @@ -28,7 +28,7 @@ open class GlucoDataActionReceiver: BroadcastReceiver() { ReceiveData.setSettings(context, bundle!!) extras.remove(Constants.SETTINGS_BUNDLE) } - ReceiveData.handleIntent(context, DataSource.PHONE, extras, true) + ReceiveData.handleIntent(context, DataSource.GDH, extras, true) } } catch (exc: Exception) { Log.e(LOG_ID, "Receive exception: " + exc.message.toString() ) diff --git a/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataReceiver.kt b/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataReceiver.kt index 3222ead0..da02eb59 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataReceiver.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/receiver/GlucoDataReceiver.kt @@ -3,7 +3,7 @@ package de.michelinside.glucodataauto.receiver import android.content.Context import android.content.Intent import android.util.Log -import de.michelinside.glucodataauto.android_auto.CarNotification +import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodatahandler.common.receiver.GlucoseDataReceiver class GlucoDataReceiver : GlucoseDataReceiver() { @@ -11,7 +11,7 @@ class GlucoDataReceiver : GlucoseDataReceiver() { override fun onReceive(context: Context, intent: Intent) { try { Log.v(LOG_ID, intent.action + " receveived: " + intent.extras.toString()) - CarNotification.initNotification(context) + GlucoDataServiceAuto.init(context) super.onReceive(context, intent) } catch (exc: Exception) { Log.e(LOG_ID, "Receive exception: " + exc.message.toString()) diff --git a/auto/src/main/java/de/michelinside/glucodataauto/receiver/XDripReceiver.kt b/auto/src/main/java/de/michelinside/glucodataauto/receiver/XDripReceiver.kt index 0efc353a..5ac7add4 100644 --- a/auto/src/main/java/de/michelinside/glucodataauto/receiver/XDripReceiver.kt +++ b/auto/src/main/java/de/michelinside/glucodataauto/receiver/XDripReceiver.kt @@ -3,7 +3,7 @@ package de.michelinside.glucodataauto.receiver import android.content.Context import android.content.Intent import android.util.Log -import de.michelinside.glucodataauto.android_auto.CarNotification +import de.michelinside.glucodataauto.GlucoDataServiceAuto import de.michelinside.glucodatahandler.common.receiver.XDripBroadcastReceiver class XDripReceiver : XDripBroadcastReceiver() { @@ -11,7 +11,7 @@ class XDripReceiver : XDripBroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { try { Log.v(LOG_ID, intent.action + " receveived: " + intent.extras.toString()) - CarNotification.initNotification(context) + GlucoDataServiceAuto.init(context) super.onReceive(context, intent) } catch (exc: Exception) { Log.e(LOG_ID, "Receive exception: " + exc.message.toString()) diff --git a/auto/src/main/res/menu/menu_items.xml b/auto/src/main/res/menu/menu_items.xml index 85f3d839..7bc8cd06 100644 --- a/auto/src/main/res/menu/menu_items.xml +++ b/auto/src/main/res/menu/menu_items.xml @@ -26,6 +26,13 @@ + + + "30" "-1" + + @string/short_value_arrow + @string/pref_status_bar_icon_trend + + + "glucose_trend" + "trend" + diff --git a/auto/src/main/res/xml/preferences.xml b/auto/src/main/res/xml/preferences.xml index 475bc8be..9af69c9b 100644 --- a/auto/src/main/res/xml/preferences.xml +++ b/auto/src/main/res/xml/preferences.xml @@ -2,7 +2,7 @@ + + + + use separate tag for not trigger onChanged events const val SHARED_PREF_INTERNAL_TAG = "GlucoDataHandlerInternalAppPrefs" @@ -104,4 +113,8 @@ object Constants { const val SHARED_PREF_NIGHTSCOUT_IOB_COB="src_ns_iob_cob" const val SHARED_PREF_DUMMY_VALUES = "dummy_values" + + // Android Auto + const val AA_MEDIA_ICON_STYLE_TREND = "trend" + const val AA_MEDIA_ICON_STYLE_GLUCOSE_TREND = "glucose_trend" } 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 7de9120b..3d56ddda 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/GlucoDataService.kt @@ -36,19 +36,25 @@ abstract class GlucoDataService(source: AppSource) : WearableListenerService() { companion object { private val LOG_ID = "GDH.GlucoDataService" private var isForegroundService = false + @JvmStatic @SuppressLint("StaticFieldLeak") - private var connection: WearPhoneConnection? = null + protected var connection: WearPhoneConnection? = null val foreground get() = isForegroundService const val NOTIFICATION_ID = 123 var appSource = AppSource.NOT_SET private var isRunning = false val running get() = isRunning + @SuppressLint("StaticFieldLeak") var service: GlucoDataService? = null - val context: Context? get() { + var context: Context? get() { if(service != null) return service!!.applicationContext - return null + return extContext + } set(value) { + extContext = value } + @SuppressLint("StaticFieldLeak") + private var extContext: Context? = null val sharedPref: SharedPreferences? get() { if (context != null) { return context!!.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) @@ -64,16 +70,17 @@ abstract class GlucoDataService(source: AppSource) : WearableListenerService() { context, cls ) + /* val sharedPref = context.getSharedPreferences( Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE - ) + )*/ serviceIntent.putExtra( Constants.SHARED_PREF_FOREGROUND_SERVICE, - // on wear foreground is true as default: on phone it is set by notification - sharedPref.getBoolean(Constants.SHARED_PREF_FOREGROUND_SERVICE, true) + // default on wear and phone + true//sharedPref.getBoolean(Constants.SHARED_PREF_FOREGROUND_SERVICE, true) ) - context.startService(serviceIntent) + context.startForegroundService(serviceIntent) } catch (exc: Exception) { Log.e( LOG_ID, diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt index 7d93f4c7..322a0a95 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt @@ -16,6 +16,7 @@ import java.math.RoundingMode import java.text.DateFormat import java.util.* import kotlin.math.abs +import kotlin.time.Duration.Companion.days object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { private const val LOG_ID = "GDH.ReceiveData" @@ -110,6 +111,12 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { } return Utils.round(deltaValue, 1) } + val deltaValueMgDl: Float get() { + if( deltaValue.isNaN() ) + return deltaValue + return Utils.round(deltaValue, 1) + } + private var isMmolValue = false val isMmol get() = isMmolValue private var use5minDelta = false @@ -148,7 +155,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { context.getString(R.string.info_label_timestamp) + ": " + DateFormat.getTimeInstance(DateFormat.DEFAULT).format(Date(time)) + "\r\n" + context.getString(R.string.info_label_alarm) + ": " + context.getString(getAlarmType().resId) + (if (forceAlarm) " ⚠" else "" ) + " (" + alarm + ")\r\n" + (if (isMmol) context.getString(R.string.info_label_raw) + ": " + rawValue + " mg/dl\r\n" else "") + - ( if (iobCobTime > 0) { + ( if (!isIobCobObsolete(1.days.inWholeSeconds.toInt())) { context.getString(R.string.info_label_iob) + ": " + getIobAsString() + " / " + context.getString(R.string.info_label_cob) + ": " + getCobAsString() + "\r\n" + context.getString(R.string.info_label_iob_cob_timestamp) + ": " + DateFormat.getTimeInstance(DateFormat.DEFAULT).format(Date(iobCobTime)) + "\r\n" } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt index a5f27adb..45b4a7ce 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt @@ -154,6 +154,12 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC } } + fun pickBestNodeId(): String? { + // Find a nearby node or pick one arbitrarily. + return connectedNodes.values.firstOrNull { it.isNearby }?.id ?: connectedNodes.values.firstOrNull()?.id + } + + private fun setNodeBatteryLevel(nodeId: String, level: Int) { if (level >= 0 && (!nodeBatteryLevel.containsKey(nodeId) || nodeBatteryLevel.getValue(nodeId) != level )) { Log.d(LOG_ID, "Setting new battery level for node " + nodeId + ": " + level + "%") @@ -170,14 +176,16 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC NotifySource.CAPILITY_INFO -> Constants.REQUEST_DATA_MESSAGE_PATH NotifySource.SETTINGS -> Constants.SETTINGS_INTENT_MESSAGE_PATH NotifySource.SOURCE_SETTINGS -> Constants.SOURCE_SETTINGS_INTENT_MESSAGE_PATH + NotifySource.LOGCAT_REQUEST -> Constants.REQUEST_LOGCAT_MESSAGE_PATH else -> Constants.GLUCODATA_INTENT_MESSAGE_PATH } - fun sendMessage(dataSource: NotifySource, extras: Bundle?, receiverId: String? = null) + fun sendMessage(dataSource: NotifySource, extras: Bundle?, ignoreReceiverId: String? = null, filterReiverId: String? = null) { try { + Log.v(LOG_ID, "sendMessage called for $dataSource filter receiver $filterReiverId ignoring receiver $ignoreReceiverId with extras $extras") if( nodesConnected && dataSource != NotifySource.NODE_BATTERY_LEVEL ) { - Log.d(LOG_ID, connectedNodes.size.toString() + " nodes found for sending message to") + Log.d(LOG_ID, connectedNodes.size.toString() + " nodes found for sending message for " + dataSource.toString()) if (extras != null && dataSource != NotifySource.BATTERY_LEVEL && BatteryReceiver.batteryPercentage > 0) { extras.putInt(BatteryReceiver.LEVEL, BatteryReceiver.batteryPercentage) } @@ -190,7 +198,7 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC connectedNodes.forEach { node -> Thread { try { - if (receiverId == null || receiverId != node.key) { + if ((ignoreReceiverId == null && filterReiverId == null) || ignoreReceiverId != node.value.id || filterReiverId == node.value.id) { if (dataSource == NotifySource.CAPILITY_INFO) Thread.sleep(1000) // wait a bit after the connection has changed sendMessage(node.value, getPath(dataSource), Utils.bundleToBytes(extras), dataSource) @@ -341,11 +349,37 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC } } } + if(p0.path == Constants.REQUEST_LOGCAT_MESSAGE_PATH) { + sendLogcat(p0.sourceNodeId) + } } catch (exc: Exception) { Log.e(LOG_ID, "onMessageReceived exception: " + exc.message.toString() ) } } + private fun sendLogcat(phoneNodeId: String) { + try { + val channelClient = Wearable.getChannelClient(context) + val channelTask = + channelClient.openChannel(phoneNodeId, Constants.LOGCAT_CHANNEL_PATH) + channelTask.addOnSuccessListener { channel -> + Thread { + try { + val outputStream = Tasks.await(channelClient.getOutputStream(channel)) + Log.d(LOG_ID, "sending Logcat") + Utils.saveLogs(outputStream) + channelClient.close(channel) + Log.d(LOG_ID, "Logcat sent") + } catch (exc: Exception) { + Log.e(LOG_ID, "sendLogcat exception: " + exc.toString()) + } + }.start() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "sendLogcat exception: " + exc.toString()) + } + } + override fun onCapabilityChanged(capabilityInfo: CapabilityInfo) { try { Log.i(LOG_ID, "onCapabilityChanged called: " + capabilityInfo.toString()) 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 f8bca263..5d68a8f1 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 @@ -10,7 +10,8 @@ enum class ChannelType(val channelId: String, val nameResId: Int, val descrResId MOBILE_SECOND("GlucoDataNotify_permanent", R.string.mobile_second_notification_name, R.string.mobile_second_notification_descr ), WORKER("worker_notification_01", R.string.worker_notification_name, R.string.worker_notification_descr, NotificationManager.IMPORTANCE_LOW ), WEAR_FOREGROUND("glucodatahandler_service_01", R.string.wear_foreground_notification_name, R.string.wear_foreground_notification_descr, NotificationManager.IMPORTANCE_LOW), - ANDROID_AUTO("GlucoDataNotify_Car", R.string.android_auto_notification_name, R.string.android_auto_notification_descr ); + ANDROID_AUTO("GlucoDataNotify_Car", R.string.android_auto_notification_name, R.string.android_auto_notification_descr ), + ANDROID_AUTO_FOREGROUND("GlucoDataAuto_foreground", R.string.mobile_foreground_notification_name, R.string.mobile_foreground_notification_descr, NotificationManager.IMPORTANCE_LOW ); } object Channels { private var notificationMgr: NotificationManager? = null diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/DataSource.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/DataSource.kt index 8d46196e..eb8a38e5 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/DataSource.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/DataSource.kt @@ -10,7 +10,8 @@ enum class DataSource(val resId: Int) { WEAR(R.string.source_wear), LIBREVIEW(R.string.source_libreview), NIGHTSCOUT(R.string.source_nightscout), - AAPS(R.string.source_aaps); + AAPS(R.string.source_aaps), + GDH(R.string.source_gdh); companion object { fun fromIndex(idx: Int): DataSource { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt index 5032ee84..7fec0c65 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt @@ -22,6 +22,10 @@ object InternalNotifier { notify(context, NotifySource.NOTIFIER_CHANGE, null) } + fun hasNotifier(notifier: NotifierInterface): Boolean { + return notifiers.contains(notifier) + } + fun notify(context: Context, notifySource: NotifySource, extras: Bundle?) { Log.d(LOG_ID, "Sending new data to " + notifiers.size.toString() + " notifier(s).") diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt index 86ec20e0..79f4e731 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt @@ -13,5 +13,6 @@ enum class NotifySource { SOURCE_SETTINGS, SOURCE_STATE_CHANGE, NOTIFIER_CHANGE, - IOB_COB_CHANGE; + IOB_COB_CHANGE, + LOGCAT_REQUEST; } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/receiver/AAPSReceiver.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/receiver/AAPSReceiver.kt index 43784c9f..397ab97e 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/receiver/AAPSReceiver.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/receiver/AAPSReceiver.kt @@ -21,7 +21,7 @@ open class AAPSReceiver: BroadcastReceiver() { private const val BG_SLOPE = "slopeArrow" // string: direction arrow as string private const val IOB_VALUE = "iob" // double private const val COB_VALUE = "cob" // double: COB [g] or -1 if N/A - private const val PUMP_STATUS = "pumpStatus" // string + //private const val PUMP_STATUS = "pumpStatus" // string private const val PROFILE_NAME = "profile" // string } @@ -37,8 +37,14 @@ open class AAPSReceiver: BroadcastReceiver() { val glucoExtras = Bundle() glucoExtras.putLong(ReceiveData.TIME, extras.getLong(BG_TIMESTAMP)) glucoExtras.putInt(ReceiveData.MGDL,mgdl.toInt()) - if(extras.containsKey(BG_UNITS) && extras.getString(BG_UNITS) == "mmol") { - glucoExtras.putFloat(ReceiveData.GLUCOSECUSTOM, GlucoDataUtils.mgToMmol(mgdl)) + if(extras.containsKey(BG_UNITS)) { + val unit = extras.getString(BG_UNITS) + if(unit == "mmol") + glucoExtras.putFloat(ReceiveData.GLUCOSECUSTOM, GlucoDataUtils.mgToMmol(mgdl)) + else if(unit == "mg/dl") + glucoExtras.putFloat(ReceiveData.GLUCOSECUSTOM, mgdl) + else + Log.w(LOG_ID, "No valid unit received: " + unit) } val slopeName = extras.getString(BG_SLOPE) var slope = Float.NaN @@ -61,9 +67,7 @@ open class AAPSReceiver: BroadcastReceiver() { } else { glucoExtras.putFloat(ReceiveData.COB, Float.NaN) } - if(extras.containsKey(PUMP_STATUS)) { - glucoExtras.putString(ReceiveData.SERIAL, extras.getString(PUMP_STATUS)) - } else if(extras.containsKey(PROFILE_NAME)) { + if(extras.containsKey(PROFILE_NAME)) { glucoExtras.putString(ReceiveData.SERIAL, extras.getString(PROFILE_NAME)) } ReceiveData.handleIntent(context, DataSource.AAPS, glucoExtras) 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 f5c72ba1..4769fb83 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 @@ -2,9 +2,11 @@ package de.michelinside.glucodatahandler.common.tasks 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.Constants @@ -34,6 +36,7 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: private var lastElapsedMinute = 0L private var isRunning: Boolean = false private var currentAlarmTime = 0L + protected var hasExactAlarmPermission = false private val elapsedTimeMinute: Long get() { return ReceiveData.getElapsedTimeMinute() @@ -170,8 +173,10 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: return delayResult } - private fun checkTimer() { + fun checkTimer() { try { + if (context == null) + return // not yet initialized val newInterval = getInterval() val newDelay = getDelay() if (initialExecution || curInterval != newInterval || curDelay != newDelay) { @@ -223,11 +228,12 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: private fun init() { if (pendingIntent == null) { Log.v(LOG_ID, "init pendingIntent") - val i = Intent(context, getAlarmReceiver()) + val intent = Intent(context, getAlarmReceiver()) + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) pendingIntent = PendingIntent.getBroadcast( context, alarmReqId, - i, + intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT ) } @@ -238,20 +244,40 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: } } - private fun startTimer() { + fun active(): Boolean { + return (alarmManager != null && pendingIntent != null) + } + + fun startTimer() { Log.v(LOG_ID, "startTimer called") val nextAlarm = getNextAlarm() if (nextAlarm != null) { init() if (alarmManager != null) { + 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 + } + } + if (currentAlarmTime != nextAlarm.timeInMillis) { + if (hasExactAlarmPermission) { + alarmManager!!.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + nextAlarm.timeInMillis, + pendingIntent!! + ) + } else { + alarmManager!!.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + nextAlarm.timeInMillis, + pendingIntent!! + ) + } currentAlarmTime = nextAlarm.timeInMillis lastElapsedMinute = elapsedTimeMinute - alarmManager!!.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - nextAlarm.timeInMillis, - pendingIntent!! - ) } else { Log.d(LOG_ID, "Ignore next alarm as it is already active") } @@ -264,7 +290,7 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: } } - private fun stopTimer() { + fun stopTimer() { if (alarmManager != null && pendingIntent != null) { Log.v(LOG_ID, "stopTimer called") alarmManager!!.cancel(pendingIntent!!) @@ -349,3 +375,17 @@ abstract class BackgroundTaskService(val alarmReqId: Int, protected val LOG_ID: } } +class AlarmPermissionReceiver: BroadcastReceiver() { + val LOG_ID = "GDH.Task.AlarmPermissionReceiver" + override fun onReceive(context: Context, intent: Intent) { + Log.i(LOG_ID, "Received broadcast " + intent.action + ": " + Utils.dumpBundle(intent.extras)) + if (TimeTaskService.active()) { + TimeTaskService.stopTimer() + TimeTaskService.startTimer() + } + if (SourceTaskService.active()) { + SourceTaskService.stopTimer() + SourceTaskService.startTimer() + } + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/ElapsedTimeTask.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/ElapsedTimeTask.kt index d67fc083..11fbd9a2 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/ElapsedTimeTask.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/ElapsedTimeTask.kt @@ -9,15 +9,20 @@ import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifySource class ElapsedTimeTask : BackgroundTask() { - private val LOG_ID = "GDH.Task.Time.ElapsedTask" - companion object { + private val LOG_ID = "GDH.Task.Time.ElapsedTask" private var relativeTimeValue = false + private var interval = 0L val relativeTime: Boolean get() {return relativeTimeValue} + fun setInterval(new_interval: Long) { + Log.d(LOG_ID, "setInterval called for new interval: " + new_interval + " - current: " + interval) + interval = new_interval + TimeTaskService.checkTimer() + } } override fun getIntervalMinute(): Long { - return 1 + return if (relativeTimeValue) 1L else interval } override fun execute(context: Context) { @@ -28,7 +33,7 @@ class ElapsedTimeTask : BackgroundTask() { } override fun active(elapsetTimeMinute: Long): Boolean { - return relativeTimeValue && elapsetTimeMinute <= 60 && InternalNotifier.getNotifierCount(NotifySource.TIME_VALUE) > 0 + return (relativeTimeValue || interval > 0) && elapsetTimeMinute <= 60 && InternalNotifier.getNotifierCount(NotifySource.TIME_VALUE) > 0 } override fun checkPreferenceChanged(sharedPreferences: SharedPreferences, key: String?, context: Context): Boolean { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/utils/Utils.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/utils/Utils.kt index 368c4e3f..4a1eafb0 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/utils/Utils.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/utils/Utils.kt @@ -6,15 +6,22 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.* +import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler import android.os.Parcel import android.provider.Settings import android.util.Log import android.util.TypedValue +import android.widget.Toast import de.michelinside.glucodatahandler.common.GlucoDataService +import de.michelinside.glucodatahandler.common.R +import java.io.FileOutputStream +import java.io.OutputStream import java.math.RoundingMode import java.security.MessageDigest +import java.util.concurrent.TimeUnit object Utils { @@ -69,15 +76,20 @@ object Utils { } fun dumpBundle(bundle: Bundle?): String { - if (bundle == null) { - return "NULL" - } - var string = "{" - for (key in bundle.keySet()) { - string += " " + key + " => " + (if (bundle[key] != null) bundle[key].toString() else "NULL") + "\r\n" + try { + if (bundle == null) { + return "NULL" + } + var string = "{" + for (key in bundle.keySet()) { + string += " " + key + " => " + (if (bundle[key] != null) bundle[key].toString() else "NULL") + "\r\n" + } + string += " }" + return string + } catch (exc: Exception) { + Log.e(LOG_ID, "dumpBundle exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) } - string += " }" - return string + return bundle.toString() } @SuppressLint("ObsoleteSdkInt") @@ -172,4 +184,64 @@ object Utils { return "" } + fun saveLogs(context: Context, uri: Uri) { + try { + Thread { + context.contentResolver.openFileDescriptor(uri, "w")?.use { + FileOutputStream(it.fileDescriptor).use { os -> + saveLogs(os) + } + } + }.start() + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving logs to file exception: " + exc.message.toString() ) + } + } + + fun saveLogs(outputStream: OutputStream) { + try { + val cmd = "logcat -t 3000" + Log.i(LOG_ID, "Getting logcat with command: $cmd") + val process = Runtime.getRuntime().exec(cmd) + val thread = Thread { + try { + Log.v(LOG_ID, "read") + val buffer = ByteArray(4 * 1024) // or other buffer size + var read: Int + while (process.inputStream.read(buffer).also { rb -> read = rb } != -1) { + Log.v(LOG_ID, "write") + outputStream.write(buffer, 0, read) + } + Log.v(LOG_ID, "flush") + outputStream.flush() + outputStream.close() + } catch (exc: Exception) { + Log.e(LOG_ID, "Writing logs exception: " + exc.message.toString() ) + } + } + thread.start() + Log.v(LOG_ID, "Waiting for saving logs") + process.waitFor(10, TimeUnit.SECONDS) + Log.v(LOG_ID, "Process alive: ${process.isAlive}") + var count = 0 + while (process.isAlive && count < 10) { + Log.w(LOG_ID, "Killing process") + process.destroy() + Thread.sleep(1000) + count++ + } + Log.v(LOG_ID, "Process exit: ${process.exitValue()}") + val text = if (process.exitValue() == 0) { + GlucoDataService.context!!.resources.getText(R.string.logcat_save_succeeded) + } else { + GlucoDataService.context!!.resources.getText(R.string.logcat_save_failed) + } + Handler(GlucoDataService.context!!.mainLooper).post { + Toast.makeText(GlucoDataService.context!!, text, Toast.LENGTH_SHORT).show() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving logs exception: " + exc.message.toString() ) + } + } + } \ No newline at end of file diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 926496d2..4a04ffc4 100644 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -294,5 +294,21 @@ AAPS AndroidAPS Konfiguration von AAPS:\n- AAPS App öffnen\n- \"Konfiguration\" öffnen\n- \"Samsung Tizen\" aktivieren + WatchDrip+ + "Aktiviere WatchDrip+ Verbindung (ohne Graph).\nWICHTIG: Aktiviere \"Enable service\" in WatchDrip+ erst nachdem es hier aktiviert wurde!" + Wieder erscheinen + Intervall, wann die Benachrichtigung wieder erscheinen soll, wenn es keinen neuen Wert gibt (0 für nie). + Stil des Icons/Hintergrundbildes für den Dummy Media Player. + Icon/image style + Telefon + Uhr + Logs speichern + Logs erfolgreich gespeichert + Fehler beim Speichern der Logs! + Logs von der Uhr erfolgreich gespeichert + Fehler beim Speichern der Logs von der Uhr! + Für alle Zeit und Intervall spezifischen Aufgaben, benötigt die App die Berechtigung für Wecker und Erinnerungen.\nDie App verändert keine vom Benutzer eingerichteten Wecker oder Erinnerung. Sie benötigt die Berechtigung nur für interne Trigger.\nNach dem Drücken von OK leitet die App Sie zur entsprechenden Berechtigungseinstellung weiter. Bitte aktivieren Sie diese Berechtigung für GlucoDataHandler. + Berechtigung für Wecker und Erinnerungen + Genaue Bearbeitung von Intervallen deaktiviert!!!\nKorrekte Funktionalität von GlucoDataHandler ist nicht gewährleistet!!!\nBitte hier drücken, um zu der entsprechenden Berechtigungseinstellung zu gelangen. diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index ea5e8e2d..a497d0cd 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -281,12 +281,12 @@ Powiadomienie wyświetlane w Android Auto IOB COB - Włączono blokowanie szumu w ustawieniach aplikacji xDrip+! \nZmień na "Ekstremalnie głośny"! + Włączono blokowanie szumu w ustawieniach aplikacji xDrip+! \nZmień na \"Ekstremalnie głośny\"! Follower (Obserwator) Juggluco - Aby odbierać wartości z Juggluco: \n- otwórz Juggluco \n- przejdź do ustawień \n- włącz Glucodata broadcast \n- włącz \"de.michelinside.glucodatahandler \" + Aby odbierać wartości z Juggluco: \n- otwórz Juggluco \n- przejdź do ustawień \n- włącz Glucodata broadcast \n- włącz \"de.michelinside.glucodatahandler \" XDrip+ - Aby odbierać wartości z xDrip+:\n- otwórz xDrip+\n- przejdź do ustawień\n- przejdź do ustawień innych aplikacji\n- włącz "Nadawaj lokalnie"\n- ustaw "Blokowanie szumów" na "Send even Extremely noisy signals"\n- włącz "Kompatybilny Broadcast"\n- sprawdź, czy pole "Identyfikuj odbiornik" jest puste lub dodaj nową linię z "de.michelinside.glucodatahandler". + Aby odbierać wartości z xDrip+:\n- otwórz xDrip+\n- przejdź do ustawień\n- przejdź do ustawień innych aplikacji\n- włącz \"Nadawaj lokalnie\"\n- ustaw \"Blokowanie szumów\" na \"Send even Extremely noisy signals\"\n- włącz \"Kompatybilny Broadcast\"\n- sprawdź, czy pole \"Identyfikuj odbiornik\" jest puste lub dodaj nową linię z \"de.michelinside.glucodatahandler\" Konfiguracja LibreLinkUp WAŻNE: to nie jest konto LibreView! Aby aktywować LibreLinkUp:\n- otwórz aplikację FreeStyle Libre i wybierz w menu Udostępnianie lub Podłączone aplikacje\n- aktywuj połączenie LibreLinkUp\n- zainstaluj LibreLinkUp ze Sklepu Play\n- skonfiguruj swoje konto i czekaj na zaproszenie Kontakt @@ -296,5 +296,21 @@ AAPS AndroidAPS Aby odbierać wartości z AAPS:\n- otwórz AAPS\n- przejdź do \"Konfiguracja\"\n- włącz \"Samsung Tizen\" + WatchDrip+ + "Włącz komunikację z WatchDrip+ (bez wykresu).\nWAŻNA UWAGA: opcję \"Enable service\" w WatchDrip+ należy włączyć dopiero po włączeniu tej opcji tutaj!" + Pokaż ponownie + Czas, po którym powiadomienie pojawi się ponownie, jeśli nie ma nowej wartości. (0 = nigdy). + Styl ikony/obrazu odtwarzacza fikcyjnych multimediów. + Styl ikony/obrazu + Telefon + Zegarek + Zapisywanie logów + Logi zapisane pomyślnie + Zapisanie logów nie powiodło się! + Logi z zegarka zapisane pomyślnie + Zapisanie logów z zegarka nie powiodło się! + W przypadku wszystkich zadań związanych z czasem i odstępami czasu aplikacja wymaga udzielenia uprawnień do ich obsługi.\nAplikacja nie dodaje ani nie zmienia żadnych alarmów i przypomnień, które ustawił użytkownik. Wymaga jedynie pozwolenia na uruchamianie wewnętrznych wyzwalaczy.\nJeśli naciśniesz OK, otwarte zostaną ustawienia uprawnień, gdzie można je nadać dla GlucoDataHandler. + Zezwalaj na ustawianie alarmów i przypomnień + Uprawnienie do ustawiania alarmów i przypomnień jest wyłączone!!!\nGlucoDataHandler może nie działać poprawnie!!!\nNaciśnij tutaj, aby przejść bezpośrednio do ustawień. diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index bd04f5d1..8d1b13de 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ GlucoDataHandler + GlucoDataHandler Sensor Value @@ -308,4 +309,20 @@ AAPS AndroidAPS To receive values from AAPS:\n- open AAPS app\n- go to \"Config Builder\"\n- enable \"Samsung Tizen\" + WatchDrip+ + "Enable WatchDrip+ communication (without graph).\nIMPORTANT: enable \"Enable service\" in WatchDrip+ after enabling this setting here!" + Reappear + Interval when the notification shall reappear if there is no new value. (0 for never). + Style of the dummy media player icon/image. + Icon/image style + Mobile + Wear + Save logs + Logs saved successfully + Failed saving logs! + Logs from wear saved successfully + Failed saving logs from wear! + For all time/interval related work, this app requires the permission for alarms & reminders.\nIt will not add or change any user reminders, it is only for internal scheduling.\nIf you press OK, you will forward to the permission setting to enable it for GlucoDataHandler. + Alarms & reminders permission + Schedule exact alarm is disabled!!!\nGlucoDataHandler may not work correct!!!\nPress here to go direct to the permission setting. diff --git a/images/playstore/de/phone_settings_forward.png b/images/playstore/de/phone_settings_forward.png index 51f1e640..30e7409d 100644 Binary files a/images/playstore/de/phone_settings_forward.png and b/images/playstore/de/phone_settings_forward.png differ diff --git a/images/playstore/phone_settings_forward.png b/images/playstore/phone_settings_forward.png index b877807d..6f39fbb0 100644 Binary files a/images/playstore/phone_settings_forward.png and b/images/playstore/phone_settings_forward.png differ diff --git a/mobile/build.gradle b/mobile/build.gradle index b5fbe85d..1c480f8a 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -66,6 +66,12 @@ android { buildFeatures { viewBinding true } + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } } dependencies { diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 3e6b69fa..4b237cef 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ - @@ -214,6 +213,14 @@ + + + + + @@ -228,7 +235,6 @@ - diff --git a/mobile/src/main/java/com/eveningoutpost/dexdrip/services/broadcastservice/models/Settings.java b/mobile/src/main/java/com/eveningoutpost/dexdrip/services/broadcastservice/models/Settings.java new file mode 100644 index 00000000..a8eebdb7 --- /dev/null +++ b/mobile/src/main/java/com/eveningoutpost/dexdrip/services/broadcastservice/models/Settings.java @@ -0,0 +1,59 @@ +package com.eveningoutpost.dexdrip.services.broadcastservice.models; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Keep; +//import lombok.Setter; +@Keep +public class Settings implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public Settings createFromParcel(Parcel in) { + return new Settings(in); + } + @Override + public Settings[] newArray(int size) { + return new Settings[size]; + } + }; + // @Setter + private long graphStart; + private long graphEnd; + private String apkName; + private boolean displayGraph; + + public Settings(Parcel in) { + apkName = in.readString(); + graphStart = in.readLong(); + graphEnd = in.readLong(); + displayGraph = in.readInt() == 1; + } + + public Settings() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(apkName); + parcel.writeLong(graphStart); + parcel.writeLong(graphEnd); + parcel.writeInt(displayGraph ? 1 : 0); + } + + // + @SuppressWarnings("all") + public long getGraphStart() { + return this.graphStart; + } + + @SuppressWarnings("all") + public boolean isDisplayGraph() { + return this.displayGraph; + } + // +} \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceMobile.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceMobile.kt index 3ddec01e..46bd666b 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceMobile.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceMobile.kt @@ -10,26 +10,34 @@ import de.michelinside.glucodatahandler.android_auto.CarModeReceiver import de.michelinside.glucodatahandler.common.* import de.michelinside.glucodatahandler.common.notifier.* import de.michelinside.glucodatahandler.common.receiver.XDripBroadcastReceiver -import de.michelinside.glucodatahandler.common.tasks.ElapsedTimeTask import de.michelinside.glucodatahandler.common.utils.GlucoDataUtils -import de.michelinside.glucodatahandler.common.utils.Utils import de.michelinside.glucodatahandler.tasker.setWearConnectionState +import de.michelinside.glucodatahandler.watch.WatchDrip import de.michelinside.glucodatahandler.widget.FloatingWidget import de.michelinside.glucodatahandler.widget.GlucoseBaseWidget class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInterface { - private val LOG_ID = "GDH.GlucoDataServiceMobile" private lateinit var floatingWidget: FloatingWidget + init { Log.d(LOG_ID, "init called") InternalNotifier.addNotifier(this, TaskerDataReceiver, mutableSetOf(NotifySource.BROADCAST,NotifySource.IOB_COB_CHANGE,NotifySource.MESSAGECLIENT,NotifySource.OBSOLETE_VALUE)) } companion object { + private val LOG_ID = "GDH.GlucoDataServiceMobile" fun start(context: Context, force: Boolean = false) { + Log.v(LOG_ID, "start called") start(AppSource.PHONE_APP, context, GlucoDataServiceMobile::class.java, force) } + + fun sendLogcatRequest() { + if(connection != null) { + Log.d(LOG_ID, "sendLogcatRequest called") + connection!!.sendMessage(NotifySource.LOGCAT_REQUEST, null, filterReiverId = connection!!.pickBestNodeId()) + } + } } override fun onCreate() { @@ -47,6 +55,7 @@ class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInt PermanentNotification.create(applicationContext) CarModeReceiver.init(applicationContext) GlucoseBaseWidget.updateWidgets(applicationContext) + WatchDrip.init(applicationContext) floatingWidget.create() } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) @@ -65,6 +74,7 @@ class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInt Log.d(LOG_ID, "onDestroy called") PermanentNotification.destroy() CarModeReceiver.cleanup(applicationContext) + WatchDrip.close(applicationContext) floatingWidget.destroy() super.onDestroy() } catch (exc: Exception) { @@ -96,21 +106,6 @@ class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInt } } - private fun sendToGlucoDataAuto(context: Context, extras: Bundle) { - val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) - if (CarModeReceiver.connected && sharedPref.getBoolean(Constants.SHARED_PREF_SEND_TO_GLUCODATAAUTO, true) && Utils.isPackageAvailable(context, Constants.PACKAGE_GLUCODATAAUTO)) { - Log.d(LOG_ID, "sendToGlucoDataAuto") - val intent = Intent(Constants.GLUCODATA_ACTION) - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - val settings = ReceiveData.getSettingsBundle() - settings.putBoolean(Constants.SHARED_PREF_RELATIVE_TIME, ElapsedTimeTask.relativeTime) - extras.putBundle(Constants.SETTINGS_BUNDLE, settings) - intent.putExtras(extras) - intent.setPackage(Constants.PACKAGE_GLUCODATAAUTO) - context.sendBroadcast(intent) - } - } - private fun sendToBangleJS(context: Context) { val send2Bangle = "require(\"Storage\").writeJSON(\"widbgjs.json\", {" + "'bg': " + ReceiveData.rawValue.toString() + "," + @@ -127,7 +122,7 @@ class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInt private fun forwardBroadcast(context: Context, extras: Bundle) { Log.v(LOG_ID, "forwardBroadcast called") val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) - sendToGlucoDataAuto(context, extras.clone() as Bundle) + CarModeReceiver.sendToGlucoDataAuto(context, extras.clone() as Bundle) if (sharedPref.getBoolean(Constants.SHARED_PREF_SEND_TO_XDRIP, false)) { val intent = Intent(Constants.XDRIP_ACTION_GLUCOSE_READING) // always sends time as start time, because it is only set, if the sensorId have changed! @@ -174,7 +169,7 @@ class GlucoDataServiceMobile: GlucoDataService(AppSource.PHONE_APP), NotifierInt if (dataSource == NotifySource.CAR_CONNECTION && CarModeReceiver.connected) { val autoExtras = ReceiveData.createExtras() if (autoExtras != null) - sendToGlucoDataAuto(context, autoExtras) + CarModeReceiver.sendToGlucoDataAuto(context, autoExtras) } if (extras != null) { if (dataSource == NotifySource.MESSAGECLIENT || dataSource == NotifySource.BROADCAST) { diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index f00c21d8..c73b31a0 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -1,5 +1,6 @@ package de.michelinside.glucodatahandler +import android.app.Activity import android.app.AlarmManager import android.content.Context import android.content.Intent @@ -18,22 +19,29 @@ import android.view.View import android.widget.Button import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuCompat import androidx.preference.PreferenceManager import de.michelinside.glucodatahandler.android_auto.CarModeReceiver +import de.michelinside.glucodatahandler.common.AppSource import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.SourceStateData -import de.michelinside.glucodatahandler.common.utils.Utils import de.michelinside.glucodatahandler.common.WearPhoneConnection import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.BitmapUtils +import de.michelinside.glucodatahandler.common.utils.Utils +import de.michelinside.glucodatahandler.watch.LogcatReceiver +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import de.michelinside.glucodatahandler.common.R as CR + class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var txtBgValue: TextView private lateinit var viewIcon: ImageView @@ -44,8 +52,10 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var txtSourceInfo: TextView private lateinit var txtBatteryOptimization: TextView private lateinit var txtHighContrastEnabled: TextView + private lateinit var txtScheduleExactAlarm: TextView private lateinit var btnSources: Button private lateinit var sharedPref: SharedPreferences + private lateinit var optionsMenu: Menu private val LOG_ID = "GDH.Main" private var requestNotificationPermission = false @@ -65,6 +75,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { txtSourceInfo = findViewById(R.id.txtSourceInfo) txtBatteryOptimization = findViewById(R.id.txtBatteryOptimization) txtHighContrastEnabled = findViewById(R.id.txtHighContrastEnabled) + txtScheduleExactAlarm = findViewById(R.id.txtScheduleExactAlarm) btnSources = findViewById(R.id.btnSources) PreferenceManager.setDefaultValues(this, R.xml.preferences, false) @@ -140,6 +151,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { NotifySource.CAR_CONNECTION, NotifySource.OBSOLETE_VALUE, NotifySource.SOURCE_STATE_CHANGE)) + checkExactAlarmPermission() checkBatteryOptimization() checkHighContrast() @@ -163,16 +175,52 @@ class MainActivity : AppCompatActivity(), NotifierInterface { return false } } + requestExactAlarmPermission() + return true + } + + private fun canScheduleExactAlarms(): Boolean { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager - if (!alarmManager.canScheduleExactAlarms()) { - Log.i(LOG_ID, "Request exact alarm permission...") - startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) - } + return alarmManager.canScheduleExactAlarms() } return true } + private fun requestExactAlarmPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms()) { + Log.i(LOG_ID, "Request exact alarm permission...") + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder + .setTitle(CR.string.request_exact_alarm_title) + .setMessage(CR.string.request_exact_alarm_summary) + .setPositiveButton(CR.string.button_ok) { dialog, which -> + startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) + } + .setNegativeButton(CR.string.button_cancel) { dialog, which -> + // Do something else. + } + val dialog: AlertDialog = builder.create() + dialog.show() + } + } + private fun checkExactAlarmPermission() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms()) { + Log.w(LOG_ID, "Schedule exact alarm is not active!!!") + txtScheduleExactAlarm.visibility = View.VISIBLE + txtScheduleExactAlarm.setOnClickListener { + startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) + } + } else { + txtScheduleExactAlarm.visibility = View.GONE + Log.i(LOG_ID, "Schedule exact alarm is active") + } + } catch (exc: Exception) { + Log.e(LOG_ID, "checkBatteryOptimization exception: " + exc.message.toString() ) + } + } + private fun checkBatteryOptimization() { try { val pm = getSystemService(POWER_SERVICE) as PowerManager @@ -216,6 +264,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { val inflater = menuInflater inflater.inflate(R.menu.menu_items, menu) MenuCompat.setGroupDividerEnabled(menu!!, true) + optionsMenu = menu return true } catch (exc: Exception) { Log.e(LOG_ID, "onCreateOptionsMenu exception: " + exc.message.toString() ) @@ -262,6 +311,19 @@ class MainActivity : AppCompatActivity(), NotifierInterface { startActivity(mailIntent) return true } + R.id.action_save_mobile_logs -> { + SaveLogs(AppSource.PHONE_APP) + return true + } + R.id.action_save_wear_logs -> { + SaveLogs(AppSource.WEAR_APP) + return true + } + R.id.group_log_title -> { + Log.v(LOG_ID, "log group selected") + val menuIt: MenuItem = optionsMenu.findItem(R.id.action_save_wear_logs) + menuIt.isEnabled = WearPhoneConnection.nodesConnected && !LogcatReceiver.isActive + } else -> return super.onOptionsItemSelected(item) } } catch (exc: Exception) { @@ -288,7 +350,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { else txtWearInfo.text = resources.getText(CR.string.activity_main_disconnected_label) if (Utils.isPackageAvailable(this, Constants.PACKAGE_GLUCODATAAUTO)) { - txtCarInfo.text = if (CarModeReceiver.connected) resources.getText(CR.string.activity_main_car_connected_label) else resources.getText(CR.string.activity_main_car_disconnected_label) + txtCarInfo.text = if (CarModeReceiver.AA_connected) resources.getText(CR.string.activity_main_car_connected_label) else resources.getText(CR.string.activity_main_car_disconnected_label) txtCarInfo.visibility = View.VISIBLE } else { txtCarInfo.visibility = View.GONE @@ -310,4 +372,45 @@ class MainActivity : AppCompatActivity(), NotifierInterface { Log.v(LOG_ID, "new intent received") update() } + + private fun SaveLogs(source: AppSource) { + try { + Log.v(LOG_ID, "Save logs called for " + source) + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + val currentDateandTime = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val fileName = "GDH_" + source + "_" + currentDateandTime + ".txt" + putExtra(Intent.EXTRA_TITLE, fileName) + } + startActivityForResult(intent, if (source == AppSource.WEAR_APP) CREATE_WEAR_FILE else CREATE_PHONE_FILE) + + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving mobile logs exception: " + exc.message.toString() ) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + try { + Log.v(LOG_ID, "onActivityResult called for requestCode: " + requestCode + " - resultCode: " + resultCode + " - data: " + Utils.dumpBundle(data?.extras)) + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + data?.data?.also { uri -> + Log.v(LOG_ID, "Save logs to " + uri) + if (requestCode == CREATE_PHONE_FILE) { + Utils.saveLogs(this, uri) + } else if(requestCode == CREATE_WEAR_FILE) { + LogcatReceiver.requestLogs(this, uri) + } + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving logs exception: " + exc.message.toString() ) + } + } + + companion object { + const val CREATE_PHONE_FILE = 1 + const val CREATE_WEAR_FILE = 2 + } } \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/android_auto/CarModeReceiver.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/android_auto/CarModeReceiver.kt index 3c5c2a52..6789c104 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/android_auto/CarModeReceiver.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/android_auto/CarModeReceiver.kt @@ -1,23 +1,60 @@ package de.michelinside.glucodatahandler.android_auto +import android.annotation.SuppressLint +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle import android.util.Log import androidx.car.app.connection.CarConnection import de.michelinside.glucodatahandler.common.* import de.michelinside.glucodatahandler.common.notifier.* +import de.michelinside.glucodatahandler.common.tasks.ElapsedTimeTask +import de.michelinside.glucodatahandler.common.utils.Utils import de.michelinside.glucodatahandler.tasker.setAndroidAutoConnectionState object CarModeReceiver { private const val LOG_ID = "GDH.CarModeReceiver" private var init = false private var car_connected = false - val connected: Boolean get() = car_connected + private var gda_enabled = false + val connected: Boolean get() { // connected to GlucoDataAuto + if (!car_connected) + return gda_enabled + return car_connected + } + + val AA_connected: Boolean get() { // connected to Android Auto + return car_connected + } + + + class GDAReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.d(LOG_ID, "onReceive called for intent " + intent + ": " + Utils.dumpBundle(intent.extras)) + gda_enabled = intent.getBooleanExtra(Constants.GLUCODATAAUTO_STATE_EXTRA, false) + if(!car_connected && gda_enabled) { + InternalNotifier.notify(context, NotifySource.CAR_CONNECTION, null) + } + } + + } + private val gdaReceiver = GDAReceiver() + @SuppressLint("UnspecifiedRegisterReceiverFlag") fun init(context: Context) { try { if(!init) { Log.v(LOG_ID, "init called") CarConnection(context).type.observeForever(CarModeReceiver::onConnectionStateUpdated) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + GlucoDataService.context!!.registerReceiver(gdaReceiver, IntentFilter(Constants.GLUCODATAAUTO_STATE_ACTION), + Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) + } else { + GlucoDataService.context!!.registerReceiver(gdaReceiver, IntentFilter(Constants.GLUCODATAAUTO_STATE_ACTION)) + } init = true } } catch (exc: Exception) { @@ -30,6 +67,7 @@ object CarModeReceiver { if (init) { Log.v(LOG_ID, "cleanup called") CarConnection(context).type.removeObserver(CarModeReceiver::onConnectionStateUpdated) + GlucoDataService.context!!.unregisterReceiver(gdaReceiver) init = false } } catch (exc: Exception) { @@ -46,18 +84,37 @@ object CarModeReceiver { else -> "Unknown car connection type" } Log.d(LOG_ID, "onConnectionStateUpdated: " + message + " (" + connectionState.toString() + ")") + val curState = connected if (connectionState == CarConnection.CONNECTION_TYPE_NOT_CONNECTED) { - Log.i(LOG_ID, "Exited Car Mode") - car_connected = false - GlucoDataService.context?.setAndroidAutoConnectionState(false) - } else { + if (car_connected) { + Log.i(LOG_ID, "Exited Car Mode") + car_connected = false + GlucoDataService.context?.setAndroidAutoConnectionState(false) + } + } else if(!car_connected){ Log.i(LOG_ID, "Entered Car Mode") car_connected = true GlucoDataService.context?.setAndroidAutoConnectionState(true) } - InternalNotifier.notify(GlucoDataService.context!!, NotifySource.CAR_CONNECTION, null) + if (curState != connected) + InternalNotifier.notify(GlucoDataService.context!!, NotifySource.CAR_CONNECTION, null) } catch (exc: Exception) { Log.e(LOG_ID, "onConnectionStateUpdated exception: " + exc.message.toString() ) } } + + fun sendToGlucoDataAuto(context: Context, extras: Bundle) { + val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + if (connected && sharedPref.getBoolean(Constants.SHARED_PREF_SEND_TO_GLUCODATAAUTO, true) && Utils.isPackageAvailable(context, Constants.PACKAGE_GLUCODATAAUTO)) { + Log.d(LOG_ID, "sendToGlucoDataAuto") + val intent = Intent(Constants.GLUCODATA_ACTION) + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + val settings = ReceiveData.getSettingsBundle() + settings.putBoolean(Constants.SHARED_PREF_RELATIVE_TIME, ElapsedTimeTask.relativeTime) + extras.putBundle(Constants.SETTINGS_BUNDLE, settings) + intent.putExtras(extras) + intent.setPackage(Constants.PACKAGE_GLUCODATAAUTO) + context.sendBroadcast(intent) + } + } } diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/watch/LogcatReceiver.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/watch/LogcatReceiver.kt new file mode 100644 index 00000000..de0c41f5 --- /dev/null +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/watch/LogcatReceiver.kt @@ -0,0 +1,128 @@ +package de.michelinside.glucodatahandler.watch + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.os.Handler +import android.util.Log +import android.widget.Toast +import com.google.android.gms.tasks.Tasks +import com.google.android.gms.wearable.ChannelClient +import com.google.android.gms.wearable.Wearable +import de.michelinside.glucodatahandler.GlucoDataServiceMobile +import de.michelinside.glucodatahandler.common.GlucoDataService +import de.michelinside.glucodatahandler.common.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.io.FileOutputStream + +object LogcatReceiver : ChannelClient.ChannelCallback() { + private val LOG_ID = "GDH.wear.LogcatReceiver" + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var finished = true + private var fileUri: Uri? = null + private var channel: ChannelClient.Channel? = null + + val isActive: Boolean get() = !finished + @SuppressLint("StaticFieldLeak") + + + fun registerChannel(context: Context, uri: Uri) { + Log.d(LOG_ID, "registerChannel called") + fileUri = uri + Wearable.getChannelClient(context).registerChannelCallback(this) + } + + override fun onChannelOpened(p0: ChannelClient.Channel) { + try { + super.onChannelOpened(p0) + Log.d(LOG_ID, "onChannelOpened") + channel = p0 + scope.launch { + try { + Log.d(LOG_ID, "receiving...") + val inputStream = Tasks.await(Wearable.getChannelClient(GlucoDataService.context!!).getInputStream(p0)) + Log.d(LOG_ID, "received, save to file " + fileUri) + GlucoDataService.context!!.contentResolver.openFileDescriptor(fileUri!!, "w")?.use { + FileOutputStream(it.fileDescriptor).use { os -> + Log.v(LOG_ID, "read") + val buffer = ByteArray(4 * 1024) // or other buffer size + var read: Int + while (inputStream.read(buffer).also { rb -> read = rb } != -1) { + Log.v(LOG_ID, "write") + os.write(buffer, 0, read) + } + Log.v(LOG_ID, "flush") + os.flush() + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "reading input exception: " + exc.message.toString() ) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onChannelOpened exception: " + exc.message.toString() ) + } + } + + override fun onInputClosed(p0: ChannelClient.Channel, i: Int, i1: Int) { + try { + super.onInputClosed(p0, i, i1) + Log.d(LOG_ID, "onInputClosed") + Wearable.getChannelClient(GlucoDataService.context!!).close(p0) + channel = null + finished = true + } catch (exc: Exception) { + Log.e(LOG_ID, "onInputClosed exception: " + exc.message.toString() ) + } + } + + fun waitFor(context: Context) { + Thread { + try { + Log.v(LOG_ID, "Waiting for receiving logs") + var count = 0 + while (!finished && count < 10) { + Thread.sleep(1000) + count++ + } + var success: Boolean + if (!finished) { + Log.w(LOG_ID, "Receiving still not finished!") + if(channel != null) + Wearable.getChannelClient(context).close(channel!!) + success = false + } else { + Log.d(LOG_ID, "Receiving finished!") + success = true + } + Wearable.getChannelClient(context).unregisterChannelCallback(this) + Log.d(LOG_ID, "unregisterChannel called") + finished = true + val text = if (success) { + GlucoDataService.context!!.resources.getText(R.string.logcat_wear_save_succeeded) + } else { + GlucoDataService.context!!.resources.getText(R.string.logcat_wear_save_failed) + } + Handler(GlucoDataService.context!!.mainLooper).post { + Toast.makeText(GlucoDataService.context!!, text, Toast.LENGTH_SHORT).show() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "waitFor exception: " + exc.message.toString() ) + } + }.start() + } + + fun requestLogs(context: Context, uri: Uri) { + if (finished) { + Log.v(LOG_ID, "request logs to " + uri) + finished = false + channel = null + registerChannel(context, uri) + GlucoDataServiceMobile.sendLogcatRequest() + waitFor(context) + } + } +} \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/watch/WatchDrip.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/watch/WatchDrip.kt new file mode 100644 index 00000000..0264869c --- /dev/null +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/watch/WatchDrip.kt @@ -0,0 +1,298 @@ +package de.michelinside.glucodatahandler.watch + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.util.Log +import de.michelinside.glucodatahandler.common.BuildConfig +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.GlucoDataService +import de.michelinside.glucodatahandler.common.ReceiveData +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifierInterface +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.GlucoDataUtils +import de.michelinside.glucodatahandler.common.utils.Utils + +object WatchDrip: SharedPreferences.OnSharedPreferenceChangeListener, NotifierInterface { + private val LOG_ID = "GDH.WatchDrip" + private var init = false + private var active = false + const val BROADCAST_SENDER_ACTION = "com.eveningoutpost.dexdrip.watch.wearintegration.BROADCAST_SERVICE_SENDER" + const val BROADCAST_RECEIVE_ACTION = "com.eveningoutpost.dexdrip.watch.wearintegration.BROADCAST_SERVICE_RECEIVER" + const val EXTRA_FUNCTION = "FUNCTION" + const val EXTRA_PACKAGE = "PACKAGE" + const val EXTRA_TYPE = "type" + const val EXTRA_MESSAGE = "message" + const val CMD_UPDATE_BG_FORCE = "update_bg_force" + const val CMD_UPDATE_BG = "update_bg" + const val CMD_ALARM = "alarm" + const val TYPE_ALERT = "BG_ALERT_TYPE" + const val TYPE_OTHER_ALERT = "BG_OTHER_ALERT_TYPE" + const val TYPE_NO_ALERT = "BG_NO_ALERT_TYPE" + val receivers = mutableSetOf() + class WatchDripReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.v(LOG_ID, "onReceive called") + handleIntent(context, intent) + } + + } + + private val watchDripReceiver = WatchDripReceiver() + + fun init(context: Context) { + try { + if (!init) { + Log.v(LOG_ID, "init called") + val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + sharedPref.registerOnSharedPreferenceChangeListener(this) + updateSettings(sharedPref) + init = true + } + } catch (exc: Exception) { + Log.e(LOG_ID, "init exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + fun close(context: Context) { + try { + if (init) { + Log.v(LOG_ID, "close called") + val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + sharedPref.unregisterOnSharedPreferenceChangeListener(this) + init = false + } + } catch (exc: Exception) { + Log.e(LOG_ID, "close exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun handleNewReceiver(pkg: String) { + if(!receivers.contains(pkg)) { + Log.i(LOG_ID, "Adding new receiver " + pkg) + receivers.add(pkg) + saveReceivers() + } + } + + private fun handleIntent(context: Context, intent: Intent) { + try { + Log.i(LOG_ID, "handleIntent called for " + intent.action + ":\n" + Utils.dumpBundle(intent.extras)) + if (intent.extras == null) { + return + } + val extras = intent.extras!! + if (!extras.containsKey(EXTRA_FUNCTION) || !extras.containsKey(EXTRA_PACKAGE)) { + Log.w(LOG_ID, "Missing mandatory extras: " + Utils.dumpBundle(intent.extras)) + return + } + val cmd = extras.getString(EXTRA_FUNCTION, "") + val pkg = extras.getString(EXTRA_PACKAGE, "") + Log.d(LOG_ID, "Command " + cmd + " received for package " + pkg) + if (CMD_UPDATE_BG_FORCE.equals(cmd) && pkg != "") { + handleNewReceiver(pkg) + sendBroadcast(context, CMD_UPDATE_BG_FORCE, pkg) + } else { + Log.d(LOG_ID, "Unknown command received: " + cmd + " received from " + pkg) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "handleIntent exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun createBundle(cmd: String): Bundle { + return when(cmd) { + CMD_ALARM -> createAlarmBundle() + else -> createBgBundle(cmd) + } + } + + private fun createBgBundle(cmd: String): Bundle { + val bundle = Bundle() + bundle.putString(EXTRA_FUNCTION, cmd) + bundle.putDouble("bg.valueMgdl", ReceiveData.rawValue.toDouble()) + bundle.putDouble("bg.deltaValueMgdl", ReceiveData.deltaValueMgDl.toDouble()) + bundle.putString("bg.deltaName", GlucoDataUtils.getDexcomLabel(ReceiveData.rate)) + bundle.putLong("bg.timeStamp", ReceiveData.time) + bundle.putBoolean("bg.isStale", ReceiveData.isObsolete(Constants.VALUE_OBSOLETE_SHORT_SEC)) + bundle.putBoolean("doMgdl", !ReceiveData.isMmol) + bundle.putBoolean("bg.isHigh", ReceiveData.getAlarmType() == ReceiveData.AlarmType.VERY_HIGH) + bundle.putBoolean("bg.isLow", ReceiveData.getAlarmType() == ReceiveData.AlarmType.VERY_LOW) + bundle.putString("pumpJSON", "{}") + if (!ReceiveData.isIobCobObsolete(Constants.VALUE_OBSOLETE_SHORT_SEC) && !ReceiveData.iob.isNaN()) { + bundle.putString("predict.IOB", ReceiveData.iobString) + bundle.putLong("predict.IOB.timeStamp", ReceiveData.iobCobTime) + } + return bundle + } + + private fun createAlarmBundle(): Bundle { + val bundle = Bundle() + bundle.putString(EXTRA_FUNCTION, CMD_ALARM) + bundle.putString(EXTRA_TYPE, getAlertType()) + bundle.putString(EXTRA_MESSAGE, getAlarmMessage()) + return bundle + } + + private fun getAlertType(): String { + return when(ReceiveData.getAlarmType()) { + ReceiveData.AlarmType.VERY_LOW, + ReceiveData.AlarmType.VERY_HIGH -> TYPE_ALERT + ReceiveData.AlarmType.LOW, + ReceiveData.AlarmType.HIGH -> TYPE_OTHER_ALERT + else -> TYPE_NO_ALERT + } + } + + private fun getAlarmMessage(): String { + return when(ReceiveData.getAlarmType()) { + ReceiveData.AlarmType.VERY_LOW -> "VERY LOW " + ReceiveData.getClucoseAsString() + ReceiveData.AlarmType.LOW -> "LOW " + ReceiveData.getClucoseAsString() + ReceiveData.AlarmType.HIGH -> "HIGH " + ReceiveData.getClucoseAsString() + ReceiveData.AlarmType.VERY_HIGH -> "VERY HIGH " + ReceiveData.getClucoseAsString() + else -> "No alarm!" + } + } + private fun sendBroadcastToReceiver(context: Context, receiver: String, bundle: Bundle) { + Log.d(LOG_ID, "Sending broadcast to " + receiver + ":\n" + Utils.dumpBundle(bundle)) + val intent = Intent(BROADCAST_SENDER_ACTION) + intent.putExtras(bundle) + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + intent.setPackage(receiver) + context.sendBroadcast(intent) + } + + private fun sendBroadcast(context: Context, cmd: String, receiver: String? = null) { + try { + if (receiver != null || receivers.size > 0) { + val bundle = createBundle(cmd) + if (receiver != null) { + sendBroadcastToReceiver(context, receiver, bundle) + } else { + receivers.forEach { + sendBroadcastToReceiver(context, it, bundle) + } + } + } else { + Log.i(LOG_ID, "No receiver found for sending broadcast") + } + } catch (exc: Exception) { + Log.e(LOG_ID, "sendBroadcast exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private fun activate() { + try { + if (GlucoDataService.context != null) { + Log.v(LOG_ID, "activate called") + loadReceivers() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + GlucoDataService.context!!.registerReceiver(watchDripReceiver, IntentFilter(BROADCAST_RECEIVE_ACTION), + Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) + } else { + GlucoDataService.context!!.registerReceiver(watchDripReceiver, IntentFilter(BROADCAST_RECEIVE_ACTION)) + } + InternalNotifier.addNotifier(GlucoDataService.context!!, this, mutableSetOf( + NotifySource.BROADCAST, + NotifySource.MESSAGECLIENT, + NotifySource.IOB_COB_CHANGE, + NotifySource.OBSOLETE_VALUE)) + active = true + if (receivers.size > 0) { + sendBroadcast(GlucoDataService.context!!, CMD_UPDATE_BG) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "activate exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun deactivate() { + try { + if (GlucoDataService.context != null && active) { + Log.v(LOG_ID, "deactivate called") + InternalNotifier.remNotifier(GlucoDataService.context!!, this) + GlucoDataService.context!!.unregisterReceiver(watchDripReceiver) + active = false + } + } catch (exc: Exception) { + Log.e(LOG_ID, "deactivate exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun loadReceivers() { + try { + val sharedExtraPref = GlucoDataService.context!!.getSharedPreferences(Constants.SHARED_PREF_EXTRAS_TAG, Context.MODE_PRIVATE) + if (sharedExtraPref.contains(Constants.SHARED_PREF_WATCHDRIP_RECEIVERS)) { + Log.i(LOG_ID, "Reading saved values...") + val savedReceivers = sharedExtraPref.getStringSet(Constants.SHARED_PREF_WATCHDRIP_RECEIVERS, HashSet()) + if (!savedReceivers.isNullOrEmpty()) { + Log.i(LOG_ID, "Loading receivers: " + savedReceivers) + receivers.addAll(savedReceivers) + } + } + if(BuildConfig.DEBUG && receivers.isEmpty()) { + receivers.add("dummy") + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Loading receivers exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun saveReceivers() { + try { + Log.d(LOG_ID, "Saving receivers") + // use own tag to prevent trigger onChange event at every time! + val sharedExtraPref = GlucoDataService.context!!.getSharedPreferences(Constants.SHARED_PREF_EXTRAS_TAG, Context.MODE_PRIVATE) + with(sharedExtraPref.edit()) { + putStringSet(Constants.SHARED_PREF_WATCHDRIP_RECEIVERS, receivers) + apply() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Saving receivers exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + private fun updateSettings(sharedPreferences: SharedPreferences) { + try { + Log.v(LOG_ID, "updateSettings called") + if (sharedPreferences.getBoolean(Constants.SHARED_PREF_WATCHDRIP, false)) + activate() + else + deactivate() + } catch (exc: Exception) { + Log.e(LOG_ID, "updateSettings exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + try { + Log.v(LOG_ID, "onSharedPreferenceChanged called for key " + key) + when(key) { + Constants.SHARED_PREF_WATCHDRIP -> { + updateSettings(sharedPreferences!!) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onSharedPreferenceChanged exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } + + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { + try { + Log.v(LOG_ID, "OnNotifyData called for source " + dataSource) + sendBroadcast(context, CMD_UPDATE_BG) + if (ReceiveData.forceAlarm && dataSource != NotifySource.IOB_COB_CHANGE && ReceiveData.alarm > 0) + sendBroadcast(context, CMD_ALARM) + } catch (exc: Exception) { + Log.e(LOG_ID, "OnNotifyData exception: " + exc.toString() + "\n" + exc.stackTraceToString() ) + } + } +} \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/widget/FloatingWidget.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/widget/FloatingWidget.kt index 5ea149e9..25dad0a6 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/widget/FloatingWidget.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/widget/FloatingWidget.kt @@ -43,34 +43,57 @@ class FloatingWidget(val context: Context) : NotifierInterface, SharedPreference @SuppressLint("InflateParams") fun create() { - Log.d(LOG_ID, "create called") - sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) - sharedPref.registerOnSharedPreferenceChangeListener(this) + try { + Log.d(LOG_ID, "create called") + sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + sharedPref.registerOnSharedPreferenceChangeListener(this) - sharedInternalPref = context.getSharedPreferences(Constants.SHARED_PREF_INTERNAL_TAG, Context.MODE_PRIVATE) + sharedInternalPref = context.getSharedPreferences(Constants.SHARED_PREF_INTERNAL_TAG, Context.MODE_PRIVATE) - //getting the widget layout from xml using layout inflater - floatingView = LayoutInflater.from(context).inflate(R.layout.floating_widget, null) - txtBgValue = floatingView.findViewById(R.id.glucose) - viewIcon = floatingView.findViewById(R.id.trendImage) - txtDelta = floatingView.findViewById(R.id.deltaText) - txtTime = floatingView.findViewById(R.id.timeText) - txtIob = floatingView.findViewById(R.id.iobText) - txtCob = floatingView.findViewById(R.id.cobText) - //setting the layout parameters - update() + //getting the widget layout from xml using layout inflater + floatingView = LayoutInflater.from(context).inflate(R.layout.floating_widget, null) + txtBgValue = floatingView.findViewById(R.id.glucose) + viewIcon = floatingView.findViewById(R.id.trendImage) + txtDelta = floatingView.findViewById(R.id.deltaText) + txtTime = floatingView.findViewById(R.id.timeText) + txtIob = floatingView.findViewById(R.id.iobText) + txtCob = floatingView.findViewById(R.id.cobText) + //setting the layout parameters + update() + } catch (exc: Exception) { + Log.e(LOG_ID, "create exception: " + exc.message.toString() ) + } } fun destroy() { - Log.d(LOG_ID, "destroy called") - sharedPref.unregisterOnSharedPreferenceChangeListener(this) - remove() + try { + Log.d(LOG_ID, "destroy called") + sharedPref.unregisterOnSharedPreferenceChangeListener(this) + remove() + } catch (exc: Exception) { + Log.e(LOG_ID, "destroy exception: " + exc.message.toString() ) + } } private fun remove() { Log.d(LOG_ID, "remove called") - InternalNotifier.remNotifier(context, this) - if (windowManager != null) windowManager?.removeView(floatingView) + try { + InternalNotifier.remNotifier(context, this) + if (windowManager != null) { + try { + with(sharedInternalPref.edit()) { + putInt(Constants.SHARED_PREF_FLOATING_WIDGET_X,params.x) + putInt(Constants.SHARED_PREF_FLOATING_WIDGET_Y,params.y) + apply() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "saving pos exception: " + exc.message.toString() ) + } + windowManager?.removeView(floatingView) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "remove exception: " + exc.message.toString() ) + } windowManager = null } @@ -182,89 +205,97 @@ class FloatingWidget(val context: Context) : NotifierInterface, SharedPreference } private fun createWindow() { - Log.d(LOG_ID, "create window") - params = WindowManager.LayoutParams( - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT - ) - params.gravity = Gravity.TOP or Gravity.START - params.x = maxOf(sharedInternalPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_X, 100), 0) - params.y = maxOf(sharedInternalPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_Y, 100), 0) + try { + Log.d(LOG_ID, "create window") + params = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT + ) + params.gravity = Gravity.TOP or Gravity.START + params.x = maxOf(sharedInternalPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_X, 100), 0) + params.y = maxOf(sharedInternalPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_Y, 100), 0) - windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager? - windowManager!!.addView(floatingView, params) + windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager? + windowManager!!.addView(floatingView, params) - val widget = floatingView.findViewById(R.id.widget) - widget.setBackgroundColor(Utils.getBackgroundColor(sharedPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_TRANSPARENCY, 3))) - widget.setOnClickListener { - Log.d(LOG_ID, "onClick called") - var launchIntent: Intent? = - context.packageManager.getLaunchIntentForPackage("tk.glucodata") - if (launchIntent == null) { - launchIntent = Intent(context, MainActivity::class.java) + val widget = floatingView.findViewById(R.id.widget) + widget.setBackgroundColor(Utils.getBackgroundColor(sharedPref.getInt(Constants.SHARED_PREF_FLOATING_WIDGET_TRANSPARENCY, 3))) + widget.setOnClickListener { + Log.d(LOG_ID, "onClick called") + var launchIntent: Intent? = + context.packageManager.getLaunchIntentForPackage("tk.glucodata") + if (launchIntent == null) { + launchIntent = Intent(context, MainActivity::class.java) + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) } - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(launchIntent) - } - widget.setOnLongClickListener { - Log.d(LOG_ID, "onLongClick called") - with(sharedPref.edit()) { - putBoolean(Constants.SHARED_PREF_FLOATING_WIDGET, false) - apply() + widget.setOnLongClickListener { + Log.d(LOG_ID, "onLongClick called") + with(sharedPref.edit()) { + putBoolean(Constants.SHARED_PREF_FLOATING_WIDGET, false) + apply() + } + remove() + true } - remove() - true - } - widget.setOnTouchListener(object : OnTouchListener { - private var initialX = 0 - private var initialY = 0 - private var initialTouchX = 0f - private var initialTouchY = 0f - private var startClickTime : Long = 0 - override fun onTouch(v: View, event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - initialX = params.x - initialY = params.y - initialTouchX = event.rawX - initialTouchY = event.rawY - startClickTime = Calendar.getInstance().timeInMillis - return true - } - MotionEvent.ACTION_UP -> { - // only check duration, if there was no movement... - with(sharedInternalPref.edit()) { - putInt(Constants.SHARED_PREF_FLOATING_WIDGET_X,params.x) - putInt(Constants.SHARED_PREF_FLOATING_WIDGET_Y,params.y) - apply() - } - if (Math.abs(params.x - initialX) < 50 && Math.abs(params.y - initialY) < 50 ) { - val duration = Calendar.getInstance().timeInMillis - startClickTime - Log.d(LOG_ID, "Duration: " + duration.toString() + " - x=" + Math.abs(params.x - initialX) + " y=" + Math.abs(params.y - initialY) ) - if (duration < 200) { - Log.d(LOG_ID, "Call onClick after " + duration.toString() + "ms") - widget.performClick() - } else if (duration > 4000) { - Log.d(LOG_ID, "Call onLongClick after " + duration.toString() + "ms") - widget.performLongClick() + widget.setOnTouchListener(object : OnTouchListener { + private var initialX = 0 + private var initialY = 0 + private var initialTouchX = 0f + private var initialTouchY = 0f + private var startClickTime : Long = 0 + override fun onTouch(v: View, event: MotionEvent): Boolean { + try { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + initialX = params.x + initialY = params.y + initialTouchX = event.rawX + initialTouchY = event.rawY + startClickTime = Calendar.getInstance().timeInMillis + return true + } + MotionEvent.ACTION_UP -> { + // only check duration, if there was no movement... + with(sharedInternalPref.edit()) { + putInt(Constants.SHARED_PREF_FLOATING_WIDGET_X,params.x) + putInt(Constants.SHARED_PREF_FLOATING_WIDGET_Y,params.y) + apply() + } + if (Math.abs(params.x - initialX) < 50 && Math.abs(params.y - initialY) < 50 ) { + val duration = Calendar.getInstance().timeInMillis - startClickTime + Log.d(LOG_ID, "Duration: " + duration.toString() + " - x=" + Math.abs(params.x - initialX) + " y=" + Math.abs(params.y - initialY) ) + if (duration < 200) { + Log.d(LOG_ID, "Call onClick after " + duration.toString() + "ms") + widget.performClick() + } else if (duration > 4000) { + Log.d(LOG_ID, "Call onLongClick after " + duration.toString() + "ms") + widget.performLongClick() + } + } + return true + } + MotionEvent.ACTION_MOVE -> { + //this code is helping the widget to move around the screen with fingers + params.x = initialX + (event.rawX - initialTouchX).toInt() + params.y = initialY + (event.rawY - initialTouchY).toInt() + windowManager!!.updateViewLayout(floatingView, params) + return true } } - return true - } - MotionEvent.ACTION_MOVE -> { - //this code is helping the widget to move around the screen with fingers - params.x = initialX + (event.rawX - initialTouchX).toInt() - params.y = initialY + (event.rawY - initialTouchY).toInt() - windowManager!!.updateViewLayout(floatingView, params) - return true + } catch (exc: Exception) { + Log.e(LOG_ID, "onTouch exception: " + exc.toString() ) } + return false } - return false - } - }) + }) + } catch (exc: Exception) { + Log.e(LOG_ID, "createWindow exception: " + exc.message.toString() ) + } } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 81c7c663..3dd63c46 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -86,6 +86,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="UselessParent"> + + + + + + + + + + app:initialExpandedChildrenCount="1"> + - @@ -77,6 +76,14 @@ + + + + + = Build.VERSION_CODES.S) { val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager - if (!alarmManager.canScheduleExactAlarms()) { - Log.i(LOG_ID, "Request exact alarm permission...") - startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) - } + return alarmManager.canScheduleExactAlarms() } return true } + private fun requestExactAlarmPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms()) { + Log.i(LOG_ID, "Request exact alarm permission...") + startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) + } + } + private fun checkExactAlarmPermission() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms()) { + Log.w(LOG_ID, "Schedule exact alarm is not active!!!") + txtScheduleExactAlarm.visibility = View.VISIBLE + txtScheduleExactAlarm.setOnClickListener { + startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) + } + } else { + txtScheduleExactAlarm.visibility = View.GONE + Log.i(LOG_ID, "Schedule exact alarm is active") + } + } catch (exc: Exception) { + Log.e(LOG_ID, "checkBatteryOptimization exception: " + exc.message.toString() ) + } + } + private fun checkHighContrast() { try { if (Utils.isHighContrastTextEnabled(this)) { diff --git a/wear/src/main/res/layout/activity_waer.xml b/wear/src/main/res/layout/activity_waer.xml index d17e5a6b..9a557c0c 100644 --- a/wear/src/main/res/layout/activity_waer.xml +++ b/wear/src/main/res/layout/activity_waer.xml @@ -73,6 +73,16 @@ android:textColor="#FFFFFF" android:textSize="14sp" /> + +