diff --git a/build.gradle b/build.gradle
index a7913a1..b225153 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,7 +44,7 @@ ext {
cyfaceEnergySettingsVersion = "0.0.0" // Automatically overwritten by CI
// Cyface dependencies
- cyfaceUtilsVersion = "3.3.7"
+ cyfaceUtilsVersion = "3.5.0"
// Android SDK versions
minSdkVersion = 21 // device support
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt b/energy_settings/src/main/java/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt
deleted file mode 100644
index 9fe9ece..0000000
--- a/energy_settings/src/main/java/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * Copyright 2019-2023 Cyface GmbH
- *
- * This file is part of the Cyface Energy Settings for Android.
- *
- * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Energy Settings for Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Energy Settings for Android. If not, see .
- */
-package de.cyface.energy_settings
-
-import android.app.Activity
-import android.app.AlertDialog
-import android.app.Dialog
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import androidx.preference.PreferenceManager
-import com.afollestad.materialdialogs.MaterialDialog
-import de.cyface.energy_settings.Constants.TAG
-import de.cyface.energy_settings.GnssDisabledWarningDialog.Companion.create
-import de.cyface.energy_settings.ProblematicManufacturerWarningDialog.Companion.create
-import java.util.*
-import kotlin.collections.LinkedHashMap
-
-/**
- * Dialog to show a warning when a phone manufacturer is identified which allows to prevent background tracking
- * by manufacturer-specific settings.
- *
- * This dialog is customized by searching for a match in a known list of manufacturer-specific energy settings pages.
- * If a match is found the required steps to adjust the settings are shown and the setting page is accessible via a
- * dialog button. Else, a generic text is shown in the dialog to help the user finding the energy settings by itself.
- *
- * This dialog also contains a button which allows the user to disable the auto-popup of this dialog.
- * This preference is not respected when the user requests guidance explicitly.
- *
- * Two implementation are available:
- *
- * 1. As `DialogFragment`. Use the constructor and use the dialog. Calls [onCreateDialog] internally.
- * 2. As [MaterialDialog]. Use the static [create] method which returns the dialog.
- *
- * @author Armin Schnabel
- * @version 2.1.0
- * @since 1.0.0
- *
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated template.
- */
-internal class ProblematicManufacturerWarningDialog(private val recipientEmail: String) : EnergySettingDialog() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-
- // Generate dialog
- val builder = AlertDialog.Builder(activity)
- builder.setTitle(titleRes)
-
- // Allow the user to express its preference to disable auto-popup of this dialog
- builder.setNegativeButton(negativeButtonRes) { _, _ ->
- onNegativeButtonCall(context)
- }
-
- // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet)
- val messageRes = if (isSony()) sonyStaminaMessageRes else genericMessageRes
-
- // Context-less scenario
- val context = context
- if (context == null) {
- Log.w(TAG, "No ProblematicManufacturerWarningDialog shown, context is null.")
- builder.setMessage(messageRes)
- return builder.create()
- }
-
- // If a device specific settings page is found, add a button to open it directly
- val deviceSpecificIntent = getDeviceSpecificIntent(context)
- if (deviceSpecificIntent != null) {
-
- // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
- val unreachableSetting = deviceSpecificIntent.key.component!!.className.equals("com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")
- return if (unreachableSetting) {
- builder.setMessage(deviceSpecificIntent.value) // Use the message for the identified intent
- builder.setPositiveButton(feedbackButtonRes) { _, _ ->
- startActivity(Intent.createChooser(intent(context, recipientEmail), getString(chooseEmailAppRes)))
- }
- builder.create()
- } else {
- val settingsIntent = deviceSpecificIntent.key
- builder.setMessage(deviceSpecificIntent.value)
- builder.setPositiveButton(openSettingsButtonRes) { _, _ -> startActivity(settingsIntent) }
- builder.create()
- }
- } else {
-
- // Generate a feedback button
- builder.setPositiveButton(feedbackButtonRes) { _, _ ->
- startActivity(Intent.createChooser(intent(context, recipientEmail), getString(chooseEmailAppRes)))
- }
-
- builder.setMessage(messageRes)
- return builder.create()
- }
- }
-
- companion object {
- /**
- * The resource pointing to the text used as title.
- */
- private val titleRes = R.string.dialog_problematic_manufacturer_warning_title
- /**
- * The resource pointing to the text of the generic dialog which is shown when no manufacturer-specific
- * energy setting page is found
- */
- private var genericMessageRes = R.string.dialog_manufacturer_warning_generic
- /**
- * The resource pointing to the text of the dialog which is shown when no manufacturer-specific
- * energy setting page is found but a sony device is detected
- */
- private val sonyStaminaMessageRes = R.string.dialog_manufacturer_warning_sony_stamina
- /**
- * The resource pointing to the text used as positive button which opens the settings.
- */
- private val openSettingsButtonRes = R.string.dialog_button_open_settings
- /**
- * The resource pointing to the text used as positive button which opens a feedback email template.
- */
- private val feedbackButtonRes = R.string.dialog_button_help
- /**
- * The resource pointing to the text used as negative button which stores the "don't show again" preference.
- */
- private val negativeButtonRes = R.string.dialog_button_do_not_show_again
-
- private fun isSony(): Boolean {
- return Build.MANUFACTURER.lowercase(Locale.ROOT) == Constants.MANUFACTURER_SONY
- }
-
- /**
- * Saves the user's preference to disable auto-popup of this dialog
- */
- private fun onNegativeButtonCall(context: Context?) {
- val preferences = PreferenceManager.getDefaultSharedPreferences(context!!)
- preferences.edit().putBoolean(Constants.PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, true).apply()
- }
-
- /**
- * Alternative, `FragmentManager`-less implementation.
- *
- * @param activity Required to show the dialog
- */
- fun create(activity: Activity, recipientEmail: String): MaterialDialog {
-
- // Generate dialog
- val dialog = MaterialDialog(activity, DIALOG_BEHAVIOUR)
- dialog.title(titleRes, null)
-
- // Allow the user to express its preference to disable auto-popup of this dialog
- dialog.negativeButton(negativeButtonRes) {
- onNegativeButtonCall(activity.applicationContext)
- }
-
- // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet)
- val messageRes = if (isSony()) sonyStaminaMessageRes else genericMessageRes
-
- // Context-less scenario
- val context = activity.applicationContext
- if (context == null) {
- Log.w(TAG, "No ProblematicManufacturerWarningDialog shown, context is null.")
- dialog.message(messageRes)
- return dialog
- }
-
- // If a device specific settings page is found, add a button to open it directly
- val deviceSpecificIntent = getDeviceSpecificIntent(context)
- if (deviceSpecificIntent != null) {
-
- // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
- val unreachable = deviceSpecificIntent.key.component!!.className.equals("com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")
- return if (unreachable) {
- dialog.positiveButton(feedbackButtonRes) {
- activity.startActivity(
- Intent.createChooser(
- intent(context, recipientEmail),
- context.getString(chooseEmailAppRes)
- )
- )
- }
- dialog.message(deviceSpecificIntent.value) // Use the message for the identified intent
- dialog
- } else {
- val settingsIntent = deviceSpecificIntent.key
- dialog.positiveButton(openSettingsButtonRes) { activity.startActivity(settingsIntent) }
- dialog.message(deviceSpecificIntent.value)
- dialog
- }
- } else {
-
- // Generate a feedback button
- dialog.positiveButton(feedbackButtonRes) {
- activity.startActivity(
- Intent.createChooser(
- intent(context, recipientEmail),
- context.getString(chooseEmailAppRes)
- )
- )
- }
-
- dialog.message(messageRes)
- return dialog
- }
- }
-
- /**
- * Searches for a match of a list of known device-specific energy setting pages.
- *
- * @param context The [Context] to check if the intent is resolvable on this device
- * @return The intent to open the settings page and the resource id of the message string which describes what to do
- * on the settings page. Returns `Null` if no intent was resolved. Always returns the first match,
- * which means the intent which was added in a later Android version if there are multiple matches.
- */
- private fun getDeviceSpecificIntent(context: Context): Map.Entry? {
- // (!) This needs to be an ordered map or else StartupAppControlActivity might be returned in favor
- // of StartupNormalAppListActivity if both exist which leads to an error [MOV-989]
- val intentMap: MutableMap = LinkedHashMap()
-
- /*
- * Huawei/Honor
- * - EMUI OS (https://de.wikipedia.org/wiki/EMUI#Versionen)
- *******************************************************************************************/
-
- // "App-Start", e.g. EMUI 9, 10
-
- // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
- // A SecurityException is thrown. I.e. the user now has to navigate on it's own to the settings.
- // On our test devices this cannot be reproduced but we see this frequently now in the Play Console.
- // But as we only see the crashes in Android 10, we leave `< Build.VERSION_CODES.Q` as it is.
- // We need to leave both intents here, so that the correct text is shown in the dialog.
- // We only disable the settings-button where `getDeviceSpecificIntent()` is called.
-
- // [MOV-989] On EMUI 10.0.0 (e.g. our P smart 2019) it finds both:
- // 1. StartupNormalAppListActivity (which works on new EMUI versions)
- // 2. StartupAppControlActivity (which crashes on new EMUI even with USE_COMPONENT permission)
- // This is why we need to keep the intents here in an ordered map with (1) earlier in the map
- // This way always (1) is returned before (2)
-
- // P20, EMUI 9, Android 9, 2019 - comment in https://stackoverflow.com/a/48641229/5815054
- // when adding the permissions above does not work with the intent above
- intentMap[Intent().setComponent(ComponentName("com.huawei.systemmanager",
- "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"))] = R.string.dialog_manufacturer_warning_huawei_app_launch
-
- // P20, EMUI 9, Android 9, 2018 - comment in https://stackoverflow.com/a/35220476/5815054
- // [STAD-280] This check should not be necessary as `StartupNormalAppListActivity` should
- // always be prioritised by the current code if both are found. However, on some Android 10
- // devices it seems like this activity is still called. Thus, we completely disable it on Q+.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- intentMap[Intent().setComponent(ComponentName("com.huawei.systemmanager",
- "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"))] = R.string.dialog_manufacturer_warning_huawei_app_launch
- }
-
- // "Protected Apps", EMUI <5, Android <7
- intentMap[Intent().setComponent(ComponentName("com.huawei.systemmanager",
- "com.huawei.systemmanager.optimize.process.ProtectActivity"))] = R.string.dialog_manufacturer_warning_huawei_protected_app
-
- /*
- * Samsung
- * - One UI (https://de.wikipedia.org/wiki/One_UI)
- * - TouchWiz (https://en.wikipedia.org/wiki/TouchWiz)
- *******************************************************************************************/
-
- // Android 7+, "Unmonitored Apps"
- // - http://sleep.urbandroid.org/documentation/faq/alarms-sleep-tracking-dont-work/#sleeping-apps
- // - Unable to test if this is necessary as we don't have a Samsung Android 7 device
- intentMap[Intent().setComponent(ComponentName("com.samsung.android.lool",
- "com.samsung.android.sm.ui.battery.BatteryActivity"))] = R.string.dialog_manufacturer_warning_samsung_device_care
-
- // Android 5-6 - https://code.briarproject.org/briar/briar/issues/1100
- // - tested: settings found and opened on Android 6.0.1 (office phone: S5 neo)
- // - by default (our phone) this was off but people might have enabled both general and app-specific option
- intentMap[Intent().setComponent(ComponentName("com.samsung.android.sm",
- "com.samsung.android.sm.ui.battery.BatteryActivity"))] = R.string.dialog_manufacturer_warning_samsung_smart_manager
-
- /*
- * Xiaomi
- * - MIUI (https://en.wikipedia.org/wiki/MIUI#Version_history)
- *******************************************************************************************/
-
- // Tested on Redmi Note 5 (Android 8.1, MIUI 10)
- intentMap[Intent().setComponent(ComponentName("com.miui.securitycenter",
- "com.miui.powercenter.PowerSettings"))] = R.string.dialog_manufacturer_warning_xiaomi_power_settings
- intentMap[Intent().setComponent(ComponentName("com.miui.securitycenter",
- "com.miui.permcenter.autostart.AutoStartManagementActivity"))] = R.string.dialog_manufacturer_warning_xiaomi_auto_start
-
- // from https://github.com/dirkam/backgroundable-android
- // Unsure if this is the correct dialog
- intentMap[Intent("miui.intent.action.POWER_HIDE_MODE_APP_LIST").addCategory(Intent.CATEGORY_DEFAULT)] = R.string.dialog_manufacturer_warning_xiaomi_power_settings
-
- // Unsure if this is the correct dialog
- intentMap[Intent("miui.intent.action.OP_AUTO_START").addCategory(Intent.CATEGORY_DEFAULT)] = R.string.dialog_manufacturer_warning_xiaomi_auto_start
-
- /*
- * Other manufacturers
- * - The following manufacturers seem to be as restrictive as Huawei etc. (see https://dontkillmyapp.com)
- * - but we have no negative reports yet from such devices, nor can we test them.
- * - For those reasons this part is uncommented but we keep this info here as it's some work to collect this
- * data.
- *******************************************************************************************/
-
- /*
- * HTC
- * - Boost+ App https://www.htc.com/de/support/htc-one-a9s/howto/830580.html
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.htc.pitroad",
- * "com.htc.pitroad.landingpage.activity.LandingPageActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *******************************************************************************************/
-
- /*
- * Oppo
- *
- * Might require the following permissions:
- * From comment in https://stackoverflow.com/a/48641229/5815054 - required for
- * com.coloros.safecenter/.startupapp.StartupAppListActivity
- *
- * From https://stackoverflow.com/a/51726040/5815054
- *
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.safecenter",
- * "com.coloros.safecenter.permission.startup.StartupAppListActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * // From https://stackoverflow.com/a/51726040/5815054 and https://github.com/dirkam/backgroundable-android
- * // Android >= 7 (ColorOS >= 3)
- * if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.safecenter",
- * "com.coloros.safecenter.startupapp.StartupAppListActivity")).setAction(Settings.
- * / * VIOLATION WARNING: ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS * /
- * ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
- * .setData(Uri.parse("package:" + context.getPackageName())),
- * R.string.dialog_manufacturer_warning_generic);
- * }
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.oppo.safe",
- * "com.oppo.safe.permission.startup.StartupAppListActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * // From https://stackoverflow.com/a/51726040/5815054
- * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
- * "com.coloros.powermanager.fuelgaue.PowerUsageModelActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
- * "com.coloros.powermanager.fuelgaue.PowerSaverModeActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
- * "com.coloros.powermanager.fuelgaue.PowerConsumptionActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Asus
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
- * "com.asus.mobilemanager.MainActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * // From https://stackoverflow.com/a/51726040/5815054
- * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
- * "com.asus.mobilemanager.entry.FunctionActivity"))
- * // From https://stackoverflow.com/a/49110392/5815054
- * .setData(Uri.parse("mobilemanager://function/entry/AutoStart")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
- * "com.asus.mobilemanager.autostart.AutoStartActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Letv
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.letv.android.letvsafe",
- * "com.letv.android.letvsafe.AutobootManageActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Meizu
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.meizu.safe",
- * "com.meizu.safe.security.SHOW_APPSEC"))
- * .addCategory(Intent.CATEGORY_DEFAULT).putExtra("packageName", BuildConfig.APPLICATION_ID),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Vivo
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.iqoo.secure",
- * "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.vivo.permissionmanager",
- * "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")),
- * R.string.dialog_manufacturer_warning_generic);
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.iqoo.secure",
- * "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Dewav
- * - https://stackoverflow.com/a/49110392/5815054
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.dewav.dwappmanager",
- * "com.dewav.dwappmanager.memory.SmartClearupWhiteList")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- /*
- * Qmobile
- * - from comment in https://stackoverflow.com/a/49110392/5815054
- *
- * intentMap.put(new Intent().setComponent(new ComponentName("com.dewav.dwappmanager",
- * "com.dewav.dwappmanager.memory.SmartClearupWhiteList")),
- * R.string.dialog_manufacturer_warning_generic);
- */
-
- // Search for a match in the list of known energy settings pages
- // Return the first match. If not this leads to [MOV-989], see note above.
- for(entry in intentMap.entries) {
- if (context.packageManager.resolveActivity(entry.key,
- PackageManager.MATCH_DEFAULT_ONLY) != null) {
- return entry
- }
- }
-
- return null
- }
- }
-}
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/TrackingSettings.kt b/energy_settings/src/main/java/de/cyface/energy_settings/TrackingSettings.kt
deleted file mode 100644
index 66ae5de..0000000
--- a/energy_settings/src/main/java/de/cyface/energy_settings/TrackingSettings.kt
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- * Copyright 2019-2022 Cyface GmbH
- *
- * This file is part of the Cyface Energy Settings for Android.
- *
- * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Energy Settings for Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Energy Settings for Android. If not, see .
- */
-package de.cyface.energy_settings
-
-import android.app.Activity
-import android.app.ActivityManager
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.location.LocationManager
-import android.os.Build
-import android.os.PowerManager
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentManager
-import androidx.preference.PreferenceManager
-import de.cyface.utils.Validate
-import java.util.Locale
-
-/**
- * Holds the API for this energy setting library.
- *
- * Offers checks and dialogs for energy settings required for background tracking.
- *
- * @author Armin Schnabel
- * @version 2.0.3
- * @since 1.0.0
- */
-object TrackingSettings {
-
- /**
- * Checks whether the energy safer mode is active *at this moment*.
- *
- * If this mode is active the GPS location service is disabled on most devices.
- *
- * API 28+
- * - due to Android documentation GPS is only disabled starting with API 28 when the display is off
- * - manufacturers can change this. On a Pixel 2 XL e.g. GPS is offline. On most other devices, too.
- * - we explicitly check if this is the case on the device and only return true if GPS gets disabled
- *
- * API < 28
- * - Some manufacturers implemented an own energy saving mode (e.g. on Honor 8, Android 7) which also kills GPS
- * when this mode is active and the display disabled. Thus, we don't also don't allow energy safer mode on lower
- * APIs.
- *
- * @param context The `Context` required to check the system settings
- * @return `True` if an energy safer mode is currently active which very likely disables the GPS service
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun isEnergySaferActive(context: Context): Boolean {
-
- val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
- val isInPowerSavingMode = powerManager.isPowerSaveMode
-
- // On newer APIs we can check if the power safer mode actually kills location tracking in background
- return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- isInPowerSavingMode
- } else isInPowerSavingMode
- && powerManager.locationPowerSaveMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY
- && powerManager.locationPowerSaveMode != PowerManager.LOCATION_MODE_NO_CHANGE
- }
-
- /**
- * Checks whether background processing is restricted by the settings.
- *
- * If this mode is enabled, the background processing is paused when the app is in background or the display is off.
- *
- * This was tested on a Pixel 2 XL device. Here this setting is disabled by default.
- * On other manufacturers, e.g. on Xiaomi's MIUI this setting is enabled by default.
- *
- * @param context The `Context` required to check the system settings
- * @return `True` if the processing is restricted
- */
- @JvmStatic
- @RequiresApi(api = Build.VERSION_CODES.P)
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun isBackgroundProcessingRestricted(context: Context): Boolean {
-
- val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
- return (activityManager.isBackgroundRestricted)
- }
-
- /**
- * Checks whether a manufacturer was identified which implements manufacturer-specific energy settings known to
- * prevent background tracking when set up wrong or even with the default settings.
- *
- * @return `True` if such a manufacturer is identified
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection") // Used by implementing app
- val isProblematicManufacturer: Boolean
- get() {
-
- return when (Build.MANUFACTURER.lowercase(Locale.ROOT)) {
- Constants.MANUFACTURER_HUAWEI, Constants.MANUFACTURER_HONOR, Constants.MANUFACTURER_SAMSUNG,
- Constants.MANUFACTURER_XIAOMI, Constants.MANUFACTURER_SONY -> true
- /* Sony STAMINA
- * - On Android 8 we may be able to check if STAMINA is disabled completely according to:
- * https://stackoverflow.com/a/50740898/5815054/, https://dontkillmyapp.com/sony
- * Settings.Secure.getInt(context.getContentResolver(), "somc.stamina_mode", 0) > 0;
- * - However, as this does not work on Android 6 we just show the warning to all SONY phones
- */
- /*
- * Other manufacturers
- * - The following manufacturers seem to be as restrictive as Huawei etc. (see
- * https://dontkillmyapp.com)
- * - but we have no negative reports yet from such devices, nor can we test them.
- * - For those reasons this part is uncommented but we keep this info here as it's some work to collect
- * this data.
- *
- * case MANUFACTURER_HTC:
- * case MANUFACTURER_OPPO:
- * case MANUFACTURER_ASUS:
- * case MANUFACTURER_LETV:
- * case MANUFACTURER_VIVO:
- * case MANUFACTURER_MEIZU:
- * case MANUFACTURER_DEWAV:
- * case MANUFACTURER_QMOBILE:
- */
- else -> false
- }
- }
-
- /**
- * Checks whether GPS is enabled in the settings.
- *
- * @param context The `Context` required to check the system settings
- * @return `True` if GPS is enabled
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun isGnssEnabled(context: Context): Boolean {
-
- val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
- return manager.isProviderEnabled(LocationManager.GPS_PROVIDER)
- }
-
- /*
- * Battery Optimization setting
- *
- * - Tests showed that this option should not be needed. (It also may sound scary to the user.)
- * - Android settings explain that this setting is only needed if the was not Doze-optimized.
- * - Our app should be Doze-optimized - which our tests indicated, too.
- *
- * else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- * && powerManager != null && !powerManager.isIgnoringBatteryOptimizations(packageName)) {
- *
- * There are two native dialogs which can be popped up:
- *
- * 1) Request direct white-listing
- * - this is not allowed in all cases and can end up in a denied play store release
- * - this would required the following permissions:
- *
- *
- * 2) Open the settings page where the user hat to white-list the app himself
- * final Intent intent = new Intent();
- * intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- * intent.setData(Uri.parse("package:" + packageName));
- * fragmentRoot.getContext().startActivity(intent);
- * }
- */
-
- /**
- * Shows a [EnergySaferWarningDialog] when [.isEnergySaferActive] is true.
- *
- * Checks [.isEnergySaferActive] before showing the dialog.
- *
- * @param context The `Context` required to check the system settings
- * @param fragment The `Fragment` where the dialog should be shown
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
- fun showEnergySaferWarningDialog(context: Context, fragment: Fragment): Boolean {
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isEnergySaferActive(context)) {
- val fragmentManager = fragment.fragmentManager
- Validate.notNull(fragmentManager)
- val dialog = EnergySaferWarningDialog()
- dialog.setTargetFragment(fragment, Constants.DIALOG_ENERGY_SAFER_WARNING_CODE)
- dialog.show(fragmentManager!!, "ENERGY_SAFER_WARNING_DIALOG")
- return true
- }
- return false
- }
-
- /**
- * Shows a [EnergySaferWarningDialog] when [.isEnergySaferActive] is true.
- *
- * Checks [.isEnergySaferActive] before showing the dialog.
- *
- * @param activity Required to show the dialog
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showEnergySaferWarningDialog(activity: Activity?): Boolean {
-
- if (activity == null || activity.isFinishing) {
- Log.w(Constants.TAG, "showEnergySaferWarningDialog: aborted, activity is null")
- return false
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isEnergySaferActive(activity.applicationContext)) {
- EnergySaferWarningDialog.create(activity).show()
- return true
- }
- return false
- }
-
- /**
- * Shows a [BackgroundProcessingRestrictionWarningDialog] when
- * [.isBackgroundProcessingRestricted] is true.
- *
- * Checks [.isBackgroundProcessingRestricted] before showing the dialog.
- *
- * @param context The `Context` required to check the system settings
- * @param fragment The `Fragment` where the dialog should be shown
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
- fun showRestrictedBackgroundProcessingWarningDialog(context: Context, fragment: Fragment): Boolean {
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isBackgroundProcessingRestricted(context)) {
- val fragmentManager = fragment.fragmentManager
- Validate.notNull(fragmentManager)
- val dialog = BackgroundProcessingRestrictionWarningDialog()
- dialog.setTargetFragment(fragment, Constants.DIALOG_BACKGROUND_RESTRICTION_WARNING_CODE)
- dialog.show(fragmentManager!!, "BACKGROUND_RESTRICTION_WARNING_DIALOG")
- return true
- }
- return false
- }
-
- /**
- * Shows a [BackgroundProcessingRestrictionWarningDialog] when
- * [.isBackgroundProcessingRestricted] is true.
- *
- * Checks [.isBackgroundProcessingRestricted] before showing the dialog.
- *
- * @param activity Required to show the dialog
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showRestrictedBackgroundProcessingWarningDialog(activity: Activity?): Boolean {
-
- if (activity == null || activity.isFinishing) {
- Log.w(Constants.TAG, "showRestrictedBackgroundProcessingWarningDialog: aborted, activity is null")
- return false
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isBackgroundProcessingRestricted(activity.applicationContext)) {
- BackgroundProcessingRestrictionWarningDialog.create(activity).show()
- return true
- }
- return false
- }
-
- /**
- * Shows a [ProblematicManufacturerWarningDialog] if [.isProblematicManufacturer] is true.
- *
- * This also checks if the user prefers not to see the dialog if he did not request guidance by himself.
- *
- * @param context The `Context` required to check the system settings
- * @param fragment The `Fragment` where the dialog should be shown
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
- * template.
- * @param force `True` if the dialog should be shown no matter of the preferences state
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
- fun showProblematicManufacturerDialog(context: Context, fragment: Fragment, force: Boolean, recipientEmail: String): Boolean {
-
- val preferences = PreferenceManager.getDefaultSharedPreferences(context)
- val alreadyShown = preferences.getBoolean(Constants.PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, false)
- if (isProblematicManufacturer && (force || !alreadyShown)) {
- val fragmentManager = fragment.fragmentManager
- Validate.notNull(fragmentManager)
- val dialog = ProblematicManufacturerWarningDialog(recipientEmail)
- dialog.setTargetFragment(fragment, Constants.DIALOG_PROBLEMATIC_MANUFACTURER_WARNING_CODE)
- dialog.show(fragmentManager!!, "PROBLEMATIC_MANUFACTURER_WARNING_DIALOG")
- return true
- }
- return false
- }
-
- /**
- * Shows a [ProblematicManufacturerWarningDialog] if [.isProblematicManufacturer] is true.
- *
- * This also checks if the user prefers not to see the dialog if he did not request guidance by himself.
- *
- * @param activity Required to show the dialog
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
- * template.
- * @param force `True` if the dialog should be shown no matter of the preferences state
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showProblematicManufacturerDialog(activity: Activity?, force: Boolean, recipientEmail: String): Boolean {
-
- if (activity == null || activity.isFinishing) {
- Log.w(Constants.TAG, "showProblematicManufacturerDialog: aborted, activity is null")
- return false
- }
-
- val preferences = PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- val alreadyShown = preferences.getBoolean(Constants.PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, false)
- if (isProblematicManufacturer && (force || !alreadyShown)) {
- ProblematicManufacturerWarningDialog.create(activity, recipientEmail).show()
- return true
- }
- return false
- }
-
- /**
- * Shows a [GnssDisabledWarningDialog] when [.isGpsEnabled] is true.
- *
- * @param context The `Context` required to check the system settings
- * @param fragment The `Fragment` where the dialog should be shown
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
- fun showGnssWarningDialog(context: Context, fragment: Fragment): Boolean {
-
- if (isGnssEnabled(context)) {
- return false
- }
-
- val fragmentManager = fragment.fragmentManager
- Validate.notNull(fragmentManager)
- val dialog = GnssDisabledWarningDialog()
- dialog.setTargetFragment(fragment, Constants.DIALOG_GPS_DISABLED_WARNING_CODE)
- dialog.show(fragmentManager!!, "GPS_DISABLED_WARNING_DIALOG")
- return true
- }
-
- /**
- * Shows a [GnssDisabledWarningDialog] when [.isGpsEnabled] is true.
- *
- * @param activity Required to show the dialog
- * @return `True` if the dialog is shown
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showGnssWarningDialog(activity: Activity?): Boolean {
-
- if (activity == null || activity.isFinishing) {
- Log.w(Constants.TAG, "showGnssWarningDialog: aborted, activity is null")
- return false
- }
- val context = activity.applicationContext
- if (isGnssEnabled(context)) {
- return false
- }
-
- GnssDisabledWarningDialog.create(activity).show()
- return true
- }
-
- /**
- * Shows a [NoGuidanceNeededDialog].
- *
- * @param fragment The `Fragment` where the dialog should be shown
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
- * template.
- */
- @JvmStatic
- @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showNoGuidanceNeededDialog(fragment: Fragment, recipientEmail: String) {
-
- val fragmentManager = fragment.fragmentManager
- Validate.notNull(fragmentManager)
- val dialog = NoGuidanceNeededDialog(recipientEmail)
- dialog.setTargetFragment(fragment, Constants.DIALOG_NO_GUIDANCE_NEEDED_DIALOG_CODE)
- dialog.show(fragmentManager!!, "NO_GUIDANCE_NEEDED_DIALOG")
- }
-
- /**
- * Shows a [NoGuidanceNeededDialog].
- *
- * @param activity Required to show the dialog
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
- * template.
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun showNoGuidanceNeededDialog(activity: Activity?, recipientEmail: String) {
-
- if (activity == null || activity.isFinishing) {
- Log.w(Constants.TAG, "showNoGuidanceNeededDialog: aborted, activity is null")
- return
- }
-
- NoGuidanceNeededDialog.create(activity, recipientEmail).show()
- }
-
- /**
- * Generates an `Intent` which can be used to open an e-mail app where a new e-mail draft is opened
- * containing a template for a feedback email.
- *
- * @param context The `Context` required to get the app version for the template's subject field
- * @param extraText The title for the text area where the user can write his message. E.g. "Your message"
- * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
- * template.
- * @return The intent
- */
- @JvmStatic
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun generateFeedbackEmailIntent(context: Context, extraText: String, recipientEmail: String): Intent {
-
- val appVersion = getAppVersion(context)
- val appAndDeviceInfo = prepareAppAndDeviceInformation(context, appVersion)
- val mailSubject = (context.getString(R.string.app_name) + " " + context.getString(R.string.feedback_email_subject) + " (" + appVersion + "-"
- + Build.VERSION.SDK_INT + ")")
-
- val emailIntent = Intent(Intent.ACTION_SEND)
- emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(recipientEmail))
- emailIntent.putExtra(Intent.EXTRA_SUBJECT, mailSubject)
- emailIntent.type = "plain/text"
- emailIntent.putExtra(Intent.EXTRA_TEXT, "$appAndDeviceInfo\n---\n$extraText:\n\n\n")
- return emailIntent
- }
-
- /**
- * Load the app version as string.
- *
- * @param context The `Context` required to get the app version
- * @return The app version as string
- */
- private fun getAppVersion(context: Context): String {
-
- val packageManager = context.packageManager
- return try {
- packageManager.getPackageInfo(context.packageName, 0).versionName
- } catch (e: PackageManager.NameNotFoundException) {
- Log.e(Constants.TAG, "App version could not be identified: $e")
- "N/A"
- }
- }
-
- /**
- * This function loads information about the app and device.
- *
- * @param context The `Context` required to get the text template
- * @param appVersion The app version as string
- * @return Device and app information
- */
- private fun prepareAppAndDeviceInformation(context: Context, appVersion: String): String {
-
- // Replace app version, commit id and device info dynamically
- return (context.getString(R.string.feedback_version_text) + ": " + appVersion + "\n"
- + context.getString(R.string.feedback_device_text) + ": " + Build.MANUFACTURER + ", "
- + Build.MODEL + " (" + Build.DEVICE + ")\n" + context.getString(R.string.feedback_android_text) + ": "
- + Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")")
- }
-
- /**
- * Dismisses all [EnergySettingDialog]s.
- *
- * You can use this in your `Activity#onPause()` method to hide all dialogs from this library
- * as the user might have changed his settings while the app is paused so you should recheck the settings
- * in `Activity#onResume()` and only show the dialogs required then.
- *
- * @param fragmentManager The `FragmentManager` required to search for open dialogs.
- */
- @JvmStatic
- @Deprecated("Only works with deprecated DialogFragment implementations. Should not be needed for MaterialDialogs.")
- @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
- fun dismissAllDialogs(fragmentManager: FragmentManager) {
-
- val fragments = fragmentManager.fragments
- for (fragment in fragments) {
- if (fragment is EnergySettingDialog) {
- fragment.dismissAllowingStateLoss()
- }
- val childFragmentManager = fragment.childFragmentManager
- @Suppress("DEPRECATION") // Ok as this is called inside the method marked as deprecated
- dismissAllDialogs(childFragmentManager)
- }
- }
-}
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/BackgroundProcessingRestrictionWarningDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/BackgroundProcessingRestrictionWarningDialog.kt
similarity index 100%
rename from energy_settings/src/main/java/de/cyface/energy_settings/BackgroundProcessingRestrictionWarningDialog.kt
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/BackgroundProcessingRestrictionWarningDialog.kt
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/Constants.java b/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt
similarity index 62%
rename from energy_settings/src/main/java/de/cyface/energy_settings/Constants.java
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt
index f98fbf1..2a11dd1 100644
--- a/energy_settings/src/main/java/de/cyface/energy_settings/Constants.java
+++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt
@@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License
* along with the Cyface Energy Settings for Android. If not, see .
*/
-package de.cyface.energy_settings;
+package de.cyface.energy_settings
/**
* Holds constants required by multiple classes.
@@ -25,29 +25,28 @@
* @version 1.0.1
* @since 1.0.0
*/
-public final class Constants {
-
- final static String TAG = "de.cyface.es";
- private final static String PACKAGE = "de.cyface.energy_settings";
+@Suppress("SpellCheckingInspection")
+object Constants {
+ const val TAG = "de.cyface.es"
+ private const val PACKAGE = "de.cyface.energy_settings"
// Dialog codes to identify the different dialogs
- public final static int DIALOG_ENERGY_SAFER_WARNING_CODE = 2019071101;
- final static int DIALOG_BACKGROUND_RESTRICTION_WARNING_CODE = 2019071102;
- final static int DIALOG_PROBLEMATIC_MANUFACTURER_WARNING_CODE = 2019071103;
- final static int DIALOG_GPS_DISABLED_WARNING_CODE = 2019071104;
- final static int DIALOG_NO_GUIDANCE_NEEDED_DIALOG_CODE = 2019071105;
+ const val DIALOG_ENERGY_SAFER_WARNING_CODE = 2019071101
+ const val DIALOG_BACKGROUND_RESTRICTION_WARNING_CODE = 2019071102
+ const val DIALOG_PROBLEMATIC_MANUFACTURER_WARNING_CODE = 2019071103
+ const val DIALOG_GPS_DISABLED_WARNING_CODE = 2019071104
+ const val DIALOG_NO_GUIDANCE_NEEDED_DIALOG_CODE = 2019071105
// Preference key for shared preferences
- final static String PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY = PACKAGE + ".manufacturer_warning_shown";
+ const val PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY = "$PACKAGE.manufacturer_warning_shown"
// Manufacturer names
- final static String MANUFACTURER_HUAWEI = "huawei";
- final static String MANUFACTURER_HONOR = "honor"; // sub brand of huawei
- final static String MANUFACTURER_SAMSUNG = "samsung";
- final static String MANUFACTURER_XIAOMI = "xiaomi";
- final static String MANUFACTURER_SONY = "sony";
-
- // The following manufacturers seem to be as restrictive as Huawei etc. (see https://dontkillmyapp.com)
+ const val MANUFACTURER_HUAWEI = "huawei"
+ const val MANUFACTURER_HONOR = "honor" // sub brand of huawei
+ const val MANUFACTURER_SAMSUNG = "samsung"
+ const val MANUFACTURER_XIAOMI = "xiaomi"
+ const val MANUFACTURER_SONY =
+ "sony" // The following manufacturers seem to be as restrictive as Huawei etc. (see https://dontkillmyapp.com)
// but we have no negative reports yet from such devices, nor can we test them.
// For those reasons this part is uncommented but we keep this info here as it's some work to collect this data.
// public final static String MANUFACTURER_HTC = "htc";
@@ -58,4 +57,4 @@ public final class Constants {
// public final static String MANUFACTURER_MEIZU = "meizu";
// public final static String MANUFACTURER_DEWAV = "dewav";
// public final static String MANUFACTURER_QMOBILE = "qmobile";
-}
+}
\ No newline at end of file
diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt
new file mode 100644
index 0000000..1a65029
--- /dev/null
+++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt
@@ -0,0 +1,20 @@
+package de.cyface.energy_settings
+
+import android.content.Context
+import androidx.core.content.edit
+import de.cyface.energy_settings.Constants.PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY
+import de.cyface.utils.AppPreferences
+
+class CustomPreferences(context: Context): AppPreferences(context) {
+
+ fun saveWarningShown(warningShown: Boolean) {
+ preferences.edit {
+ putBoolean(PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, warningShown)
+ apply()
+ }
+ }
+
+ fun getWarningShown(): Boolean {
+ return preferences.getBoolean(PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, false)
+ }
+}
\ No newline at end of file
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/EnergySaferWarningDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/EnergySaferWarningDialog.kt
similarity index 100%
rename from energy_settings/src/main/java/de/cyface/energy_settings/EnergySaferWarningDialog.kt
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/EnergySaferWarningDialog.kt
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/EnergySettingDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/EnergySettingDialog.kt
similarity index 100%
rename from energy_settings/src/main/java/de/cyface/energy_settings/EnergySettingDialog.kt
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/EnergySettingDialog.kt
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/GnssDisabledWarningDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/GnssDisabledWarningDialog.kt
similarity index 100%
rename from energy_settings/src/main/java/de/cyface/energy_settings/GnssDisabledWarningDialog.kt
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/GnssDisabledWarningDialog.kt
diff --git a/energy_settings/src/main/java/de/cyface/energy_settings/NoGuidanceNeededDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/NoGuidanceNeededDialog.kt
similarity index 100%
rename from energy_settings/src/main/java/de/cyface/energy_settings/NoGuidanceNeededDialog.kt
rename to energy_settings/src/main/kotlin/de/cyface/energy_settings/NoGuidanceNeededDialog.kt
diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt
new file mode 100644
index 0000000..e1301a6
--- /dev/null
+++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2019-2023 Cyface GmbH
+ *
+ * This file is part of the Cyface Energy Settings for Android.
+ *
+ * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Cyface Energy Settings for Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the Cyface Energy Settings for Android. If not, see .
+ */
+package de.cyface.energy_settings
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import com.afollestad.materialdialogs.MaterialDialog
+import de.cyface.energy_settings.Constants.TAG
+import de.cyface.energy_settings.GnssDisabledWarningDialog.Companion.create
+import de.cyface.energy_settings.ProblematicManufacturerWarningDialog.Companion.create
+import java.util.Locale
+
+/**
+ * Dialog to show a warning when a phone manufacturer is identified which allows to prevent background tracking
+ * by manufacturer-specific settings.
+ *
+ * This dialog is customized by searching for a match in a known list of manufacturer-specific energy settings pages.
+ * If a match is found the required steps to adjust the settings are shown and the setting page is accessible via a
+ * dialog button. Else, a generic text is shown in the dialog to help the user finding the energy settings by itself.
+ *
+ * This dialog also contains a button which allows the user to disable the auto-popup of this dialog.
+ * This preference is not respected when the user requests guidance explicitly.
+ *
+ * Two implementation are available:
+ *
+ * 1. As `DialogFragment`. Use the constructor and use the dialog. Calls [onCreateDialog] internally.
+ * 2. As [MaterialDialog]. Use the static [create] method which returns the dialog.
+ *
+ * @author Armin Schnabel
+ * @version 2.1.0
+ * @since 1.0.0
+ *
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated template.
+ */
+internal class ProblematicManufacturerWarningDialog(private val recipientEmail: String) :
+ EnergySettingDialog() {
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+
+ // Generate dialog
+ val builder = AlertDialog.Builder(activity)
+ builder.setTitle(titleRes)
+
+ // Allow the user to express its preference to disable auto-popup of this dialog
+ builder.setNegativeButton(negativeButtonRes) { _, _ ->
+ onNegativeButtonCall(context)
+ }
+
+ // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet)
+ val messageRes = if (isSony()) sonyStaminaMessageRes else genericMessageRes
+
+ // Context-less scenario
+ val context = context
+ if (context == null) {
+ Log.w(TAG, "No ProblematicManufacturerWarningDialog shown, context is null.")
+ builder.setMessage(messageRes)
+ return builder.create()
+ }
+
+ // If a device specific settings page is found, add a button to open it directly
+ val deviceSpecificIntent = getDeviceSpecificIntent(context)
+ if (deviceSpecificIntent != null) {
+
+ // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
+ val unreachableSetting =
+ deviceSpecificIntent.key.component!!.className.equals("com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")
+ return if (unreachableSetting) {
+ builder.setMessage(deviceSpecificIntent.value) // Use the message for the identified intent
+ builder.setPositiveButton(feedbackButtonRes) { _, _ ->
+ startActivity(
+ Intent.createChooser(
+ intent(context, recipientEmail),
+ getString(chooseEmailAppRes)
+ )
+ )
+ }
+ builder.create()
+ } else {
+ val settingsIntent = deviceSpecificIntent.key
+ builder.setMessage(deviceSpecificIntent.value)
+ builder.setPositiveButton(openSettingsButtonRes) { _, _ ->
+ startActivity(
+ settingsIntent
+ )
+ }
+ builder.create()
+ }
+ } else {
+
+ // Generate a feedback button
+ builder.setPositiveButton(feedbackButtonRes) { _, _ ->
+ startActivity(
+ Intent.createChooser(
+ intent(context, recipientEmail),
+ getString(chooseEmailAppRes)
+ )
+ )
+ }
+
+ builder.setMessage(messageRes)
+ return builder.create()
+ }
+ }
+
+ companion object {
+ /**
+ * The resource pointing to the text used as title.
+ */
+ private val titleRes = R.string.dialog_problematic_manufacturer_warning_title
+
+ /**
+ * The resource pointing to the text of the generic dialog which is shown when no manufacturer-specific
+ * energy setting page is found
+ */
+ private var genericMessageRes = R.string.dialog_manufacturer_warning_generic
+
+ /**
+ * The resource pointing to the text of the dialog which is shown when no manufacturer-specific
+ * energy setting page is found but a sony device is detected
+ */
+ private val sonyStaminaMessageRes = R.string.dialog_manufacturer_warning_sony_stamina
+
+ /**
+ * The resource pointing to the text used as positive button which opens the settings.
+ */
+ private val openSettingsButtonRes = R.string.dialog_button_open_settings
+
+ /**
+ * The resource pointing to the text used as positive button which opens a feedback email template.
+ */
+ private val feedbackButtonRes = R.string.dialog_button_help
+
+ /**
+ * The resource pointing to the text used as negative button which stores the "don't show again" preference.
+ */
+ private val negativeButtonRes = R.string.dialog_button_do_not_show_again
+
+ private fun isSony(): Boolean {
+ return Build.MANUFACTURER.lowercase(Locale.ROOT) == Constants.MANUFACTURER_SONY
+ }
+
+ /**
+ * Saves the user's preference to disable auto-popup of this dialog
+ */
+ private fun onNegativeButtonCall(context: Context?) {
+ CustomPreferences(context!!).saveWarningShown(true)
+ }
+
+ /**
+ * Alternative, `FragmentManager`-less implementation.
+ *
+ * @param activity Required to show the dialog
+ */
+ fun create(activity: Activity, recipientEmail: String): MaterialDialog {
+
+ // Generate dialog
+ val dialog = MaterialDialog(activity, DIALOG_BEHAVIOUR)
+ dialog.title(titleRes, null)
+
+ // Allow the user to express its preference to disable auto-popup of this dialog
+ dialog.negativeButton(negativeButtonRes) {
+ onNegativeButtonCall(activity.applicationContext)
+ }
+
+ // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet)
+ val messageRes = if (isSony()) sonyStaminaMessageRes else genericMessageRes
+
+ // Context-less scenario
+ val context = activity.applicationContext
+ if (context == null) {
+ Log.w(TAG, "No ProblematicManufacturerWarningDialog shown, context is null.")
+ dialog.message(messageRes)
+ return dialog
+ }
+
+ // If a device specific settings page is found, add a button to open it directly
+ val deviceSpecificIntent = getDeviceSpecificIntent(context)
+ if (deviceSpecificIntent != null) {
+
+ // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
+ val unreachable =
+ deviceSpecificIntent.key.component!!.className.equals("com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")
+ return if (unreachable) {
+ dialog.positiveButton(feedbackButtonRes) {
+ activity.startActivity(
+ Intent.createChooser(
+ intent(context, recipientEmail),
+ context.getString(chooseEmailAppRes)
+ )
+ )
+ }
+ dialog.message(deviceSpecificIntent.value) // Use the message for the identified intent
+ dialog
+ } else {
+ val settingsIntent = deviceSpecificIntent.key
+ dialog.positiveButton(openSettingsButtonRes) {
+ activity.startActivity(
+ settingsIntent
+ )
+ }
+ dialog.message(deviceSpecificIntent.value)
+ dialog
+ }
+ } else {
+
+ // Generate a feedback button
+ dialog.positiveButton(feedbackButtonRes) {
+ activity.startActivity(
+ Intent.createChooser(
+ intent(context, recipientEmail),
+ context.getString(chooseEmailAppRes)
+ )
+ )
+ }
+
+ dialog.message(messageRes)
+ return dialog
+ }
+ }
+
+ /**
+ * Searches for a match of a list of known device-specific energy setting pages.
+ *
+ * @param context The [Context] to check if the intent is resolvable on this device
+ * @return The intent to open the settings page and the resource id of the message string which describes what to do
+ * on the settings page. Returns `Null` if no intent was resolved. Always returns the first match,
+ * which means the intent which was added in a later Android version if there are multiple matches.
+ */
+ private fun getDeviceSpecificIntent(context: Context): Map.Entry? {
+ // (!) This needs to be an ordered map or else StartupAppControlActivity might be returned in favor
+ // of StartupNormalAppListActivity if both exist which leads to an error [MOV-989]
+ val intentMap: MutableMap = LinkedHashMap()
+
+ /*
+ * Huawei/Honor
+ * - EMUI OS (https://de.wikipedia.org/wiki/EMUI#Versionen)
+ *******************************************************************************************/
+
+ // "App-Start", e.g. EMUI 9, 10
+
+ // [STAD-492] Since 2021 Huawei started to deny opening the Huawei "App Launch" setting via intent.
+ // A SecurityException is thrown. I.e. the user now has to navigate on it's own to the settings.
+ // On our test devices this cannot be reproduced but we see this frequently now in the Play Console.
+ // But as we only see the crashes in Android 10, we leave `< Build.VERSION_CODES.Q` as it is.
+ // We need to leave both intents here, so that the correct text is shown in the dialog.
+ // We only disable the settings-button where `getDeviceSpecificIntent()` is called.
+
+ // [MOV-989] On EMUI 10.0.0 (e.g. our P smart 2019) it finds both:
+ // 1. StartupNormalAppListActivity (which works on new EMUI versions)
+ // 2. StartupAppControlActivity (which crashes on new EMUI even with USE_COMPONENT permission)
+ // This is why we need to keep the intents here in an ordered map with (1) earlier in the map
+ // This way always (1) is returned before (2)
+
+ // P20, EMUI 9, Android 9, 2019 - comment in https://stackoverflow.com/a/48641229/5815054
+ // when adding the permissions above does not work with the intent above
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.huawei.systemmanager",
+ "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_huawei_app_launch
+
+ // P20, EMUI 9, Android 9, 2018 - comment in https://stackoverflow.com/a/35220476/5815054
+ // [STAD-280] This check should not be necessary as `StartupNormalAppListActivity` should
+ // always be prioritised by the current code if both are found. However, on some Android 10
+ // devices it seems like this activity is still called. Thus, we completely disable it on Q+.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.huawei.systemmanager",
+ "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_huawei_app_launch
+ }
+
+ // "Protected Apps", EMUI <5, Android <7
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.huawei.systemmanager",
+ "com.huawei.systemmanager.optimize.process.ProtectActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_huawei_protected_app
+
+ /*
+ * Samsung
+ * - One UI (https://de.wikipedia.org/wiki/One_UI)
+ * - TouchWiz (https://en.wikipedia.org/wiki/TouchWiz)
+ *******************************************************************************************/
+
+ // Android 7+, "Unmonitored Apps"
+ // - http://sleep.urbandroid.org/documentation/faq/alarms-sleep-tracking-dont-work/#sleeping-apps
+ // - Unable to test if this is necessary as we don't have a Samsung Android 7 device
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.samsung.android.lool",
+ "com.samsung.android.sm.ui.battery.BatteryActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_samsung_device_care
+
+ // Android 5-6 - https://code.briarproject.org/briar/briar/issues/1100
+ // - tested: settings found and opened on Android 6.0.1 (office phone: S5 neo)
+ // - by default (our phone) this was off but people might have enabled both general and app-specific option
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.samsung.android.sm",
+ "com.samsung.android.sm.ui.battery.BatteryActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_samsung_smart_manager
+
+ /*
+ * Xiaomi
+ * - MIUI (https://en.wikipedia.org/wiki/MIUI#Version_history)
+ *******************************************************************************************/
+
+ // Tested on Redmi Note 5 (Android 8.1, MIUI 10)
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.miui.securitycenter",
+ "com.miui.powercenter.PowerSettings"
+ )
+ )] = R.string.dialog_manufacturer_warning_xiaomi_power_settings
+ intentMap[Intent().setComponent(
+ ComponentName(
+ "com.miui.securitycenter",
+ "com.miui.permcenter.autostart.AutoStartManagementActivity"
+ )
+ )] = R.string.dialog_manufacturer_warning_xiaomi_auto_start
+
+ // from https://github.com/dirkam/backgroundable-android
+ // Unsure if this is the correct dialog
+ intentMap[Intent("miui.intent.action.POWER_HIDE_MODE_APP_LIST").addCategory(Intent.CATEGORY_DEFAULT)] =
+ R.string.dialog_manufacturer_warning_xiaomi_power_settings
+
+ // Unsure if this is the correct dialog
+ intentMap[Intent("miui.intent.action.OP_AUTO_START").addCategory(Intent.CATEGORY_DEFAULT)] =
+ R.string.dialog_manufacturer_warning_xiaomi_auto_start
+
+ /*
+ * Other manufacturers
+ * - The following manufacturers seem to be as restrictive as Huawei etc. (see https://dontkillmyapp.com)
+ * - but we have no negative reports yet from such devices, nor can we test them.
+ * - For those reasons this part is uncommented but we keep this info here as it's some work to collect this
+ * data.
+ *******************************************************************************************/
+
+ /*
+ * HTC
+ * - Boost+ App https://www.htc.com/de/support/htc-one-a9s/howto/830580.html
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.htc.pitroad",
+ * "com.htc.pitroad.landingpage.activity.LandingPageActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *******************************************************************************************/
+
+ /*
+ * Oppo
+ *
+ * Might require the following permissions:
+ * From comment in https://stackoverflow.com/a/48641229/5815054 - required for
+ * com.coloros.safecenter/.startupapp.StartupAppListActivity
+ *
+ * From https://stackoverflow.com/a/51726040/5815054
+ *
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.safecenter",
+ * "com.coloros.safecenter.permission.startup.StartupAppListActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * // From https://stackoverflow.com/a/51726040/5815054 and https://github.com/dirkam/backgroundable-android
+ * // Android >= 7 (ColorOS >= 3)
+ * if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.safecenter",
+ * "com.coloros.safecenter.startupapp.StartupAppListActivity")).setAction(Settings.
+ * / * VIOLATION WARNING: ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS * /
+ * ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
+ * .setData(Uri.parse("package:" + context.getPackageName())),
+ * R.string.dialog_manufacturer_warning_generic);
+ * }
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.oppo.safe",
+ * "com.oppo.safe.permission.startup.StartupAppListActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * // From https://stackoverflow.com/a/51726040/5815054
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
+ * "com.coloros.powermanager.fuelgaue.PowerUsageModelActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
+ * "com.coloros.powermanager.fuelgaue.PowerSaverModeActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.coloros.oppoguardelf",
+ * "com.coloros.powermanager.fuelgaue.PowerConsumptionActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Asus
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
+ * "com.asus.mobilemanager.MainActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * // From https://stackoverflow.com/a/51726040/5815054
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
+ * "com.asus.mobilemanager.entry.FunctionActivity"))
+ * // From https://stackoverflow.com/a/49110392/5815054
+ * .setData(Uri.parse("mobilemanager://function/entry/AutoStart")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.asus.mobilemanager",
+ * "com.asus.mobilemanager.autostart.AutoStartActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Letv
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.letv.android.letvsafe",
+ * "com.letv.android.letvsafe.AutobootManageActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Meizu
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.meizu.safe",
+ * "com.meizu.safe.security.SHOW_APPSEC"))
+ * .addCategory(Intent.CATEGORY_DEFAULT).putExtra("packageName", BuildConfig.APPLICATION_ID),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Vivo
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.iqoo.secure",
+ * "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.vivo.permissionmanager",
+ * "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")),
+ * R.string.dialog_manufacturer_warning_generic);
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.iqoo.secure",
+ * "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Dewav
+ * - https://stackoverflow.com/a/49110392/5815054
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.dewav.dwappmanager",
+ * "com.dewav.dwappmanager.memory.SmartClearupWhiteList")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ /*
+ * Qmobile
+ * - from comment in https://stackoverflow.com/a/49110392/5815054
+ *
+ * intentMap.put(new Intent().setComponent(new ComponentName("com.dewav.dwappmanager",
+ * "com.dewav.dwappmanager.memory.SmartClearupWhiteList")),
+ * R.string.dialog_manufacturer_warning_generic);
+ */
+
+ // Search for a match in the list of known energy settings pages
+ // Return the first match. If not this leads to [MOV-989], see note above.
+ for (entry in intentMap.entries) {
+ if (context.packageManager.resolveActivity(
+ entry.key,
+ PackageManager.MATCH_DEFAULT_ONLY
+ ) != null
+ ) {
+ return entry
+ }
+ }
+
+ return null
+ }
+ }
+}
diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt
new file mode 100644
index 0000000..e19b892
--- /dev/null
+++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt
@@ -0,0 +1,536 @@
+/*
+ * Copyright 2019-2023 Cyface GmbH
+ *
+ * This file is part of the Cyface Energy Settings for Android.
+ *
+ * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Cyface Energy Settings for Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the Cyface Energy Settings for Android. If not, see .
+ */
+package de.cyface.energy_settings
+
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.location.LocationManager
+import android.os.Build
+import android.os.PowerManager
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import de.cyface.utils.Validate
+import java.util.Locale
+
+/**
+ * Holds the API for this energy setting library.
+ *
+ * Offers checks and dialogs for energy settings required for background tracking.
+ *
+ * @author Armin Schnabel
+ * @version 2.0.3
+ * @since 1.0.0
+ */
+object TrackingSettings {
+
+ /**
+ * Checks whether the energy safer mode is active *at this moment*.
+ *
+ * If this mode is active the GPS location service is disabled on most devices.
+ *
+ * API 28+
+ * - due to Android documentation GPS is only disabled starting with API 28 when the display is off
+ * - manufacturers can change this. On a Pixel 2 XL e.g. GPS is offline. On most other devices, too.
+ * - we explicitly check if this is the case on the device and only return true if GPS gets disabled
+ *
+ * API < 28
+ * - Some manufacturers implemented an own energy saving mode (e.g. on Honor 8, Android 7) which also kills GPS
+ * when this mode is active and the display disabled. Thus, we don't also don't allow energy safer mode on lower
+ * APIs.
+ *
+ * @param context The `Context` required to check the system settings
+ * @return `True` if an energy safer mode is currently active which very likely disables the GPS service
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun isEnergySaferActive(context: Context): Boolean {
+
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ val isInPowerSavingMode = powerManager.isPowerSaveMode
+
+ // On newer APIs we can check if the power safer mode actually kills location tracking in background
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ isInPowerSavingMode
+ } else isInPowerSavingMode
+ && powerManager.locationPowerSaveMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY
+ && powerManager.locationPowerSaveMode != PowerManager.LOCATION_MODE_NO_CHANGE
+ }
+
+ /**
+ * Checks whether background processing is restricted by the settings.
+ *
+ * If this mode is enabled, the background processing is paused when the app is in background or the display is off.
+ *
+ * This was tested on a Pixel 2 XL device. Here this setting is disabled by default.
+ * On other manufacturers, e.g. on Xiaomi's MIUI this setting is enabled by default.
+ *
+ * @param context The `Context` required to check the system settings
+ * @return `True` if the processing is restricted
+ */
+ @JvmStatic
+ @RequiresApi(api = Build.VERSION_CODES.P)
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun isBackgroundProcessingRestricted(context: Context): Boolean {
+
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ return (activityManager.isBackgroundRestricted)
+ }
+
+ /**
+ * Checks whether a manufacturer was identified which implements manufacturer-specific energy settings known to
+ * prevent background tracking when set up wrong or even with the default settings.
+ *
+ * @return `True` if such a manufacturer is identified
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection") // Used by implementing app
+ val isProblematicManufacturer: Boolean
+ get() {
+
+ return when (Build.MANUFACTURER.lowercase(Locale.ROOT)) {
+ Constants.MANUFACTURER_HUAWEI, Constants.MANUFACTURER_HONOR, Constants.MANUFACTURER_SAMSUNG,
+ Constants.MANUFACTURER_XIAOMI, Constants.MANUFACTURER_SONY -> true
+ /* Sony STAMINA
+ * - On Android 8 we may be able to check if STAMINA is disabled completely according to:
+ * https://stackoverflow.com/a/50740898/5815054/, https://dontkillmyapp.com/sony
+ * Settings.Secure.getInt(context.getContentResolver(), "somc.stamina_mode", 0) > 0;
+ * - However, as this does not work on Android 6 we just show the warning to all SONY phones
+ */
+ /*
+ * Other manufacturers
+ * - The following manufacturers seem to be as restrictive as Huawei etc. (see
+ * https://dontkillmyapp.com)
+ * - but we have no negative reports yet from such devices, nor can we test them.
+ * - For those reasons this part is uncommented but we keep this info here as it's some work to collect
+ * this data.
+ *
+ * case MANUFACTURER_HTC:
+ * case MANUFACTURER_OPPO:
+ * case MANUFACTURER_ASUS:
+ * case MANUFACTURER_LETV:
+ * case MANUFACTURER_VIVO:
+ * case MANUFACTURER_MEIZU:
+ * case MANUFACTURER_DEWAV:
+ * case MANUFACTURER_QMOBILE:
+ */
+ else -> false
+ }
+ }
+
+ /**
+ * Checks whether GPS is enabled in the settings.
+ *
+ * @param context The `Context` required to check the system settings
+ * @return `True` if GPS is enabled
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun isGnssEnabled(context: Context): Boolean {
+
+ val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ return manager.isProviderEnabled(LocationManager.GPS_PROVIDER)
+ }
+
+ /*
+ * Battery Optimization setting
+ *
+ * - Tests showed that this option should not be needed. (It also may sound scary to the user.)
+ * - Android settings explain that this setting is only needed if the was not Doze-optimized.
+ * - Our app should be Doze-optimized - which our tests indicated, too.
+ *
+ * else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ * && powerManager != null && !powerManager.isIgnoringBatteryOptimizations(packageName)) {
+ *
+ * There are two native dialogs which can be popped up:
+ *
+ * 1) Request direct white-listing
+ * - this is not allowed in all cases and can end up in a denied play store release
+ * - this would required the following permissions:
+ *
+ *
+ * 2) Open the settings page where the user hat to white-list the app himself
+ * final Intent intent = new Intent();
+ * intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ * intent.setData(Uri.parse("package:" + packageName));
+ * fragmentRoot.getContext().startActivity(intent);
+ * }
+ */
+
+ /**
+ * Shows a [EnergySaferWarningDialog] when [.isEnergySaferActive] is true.
+ *
+ * Checks [.isEnergySaferActive] before showing the dialog.
+ *
+ * @param context The `Context` required to check the system settings
+ * @param fragment The `Fragment` where the dialog should be shown
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
+ fun showEnergySaferWarningDialog(context: Context, fragment: Fragment): Boolean {
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isEnergySaferActive(context)) {
+ val fragmentManager = fragment.fragmentManager
+ Validate.notNull(fragmentManager)
+ val dialog = EnergySaferWarningDialog()
+ dialog.setTargetFragment(fragment, Constants.DIALOG_ENERGY_SAFER_WARNING_CODE)
+ dialog.show(fragmentManager!!, "ENERGY_SAFER_WARNING_DIALOG")
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [EnergySaferWarningDialog] when [.isEnergySaferActive] is true.
+ *
+ * Checks [.isEnergySaferActive] before showing the dialog.
+ *
+ * @param activity Required to show the dialog
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showEnergySaferWarningDialog(activity: Activity?): Boolean {
+
+ if (activity == null || activity.isFinishing) {
+ Log.w(Constants.TAG, "showEnergySaferWarningDialog: aborted, activity is null")
+ return false
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isEnergySaferActive(activity.applicationContext)) {
+ EnergySaferWarningDialog.create(activity).show()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [BackgroundProcessingRestrictionWarningDialog] when
+ * [.isBackgroundProcessingRestricted] is true.
+ *
+ * Checks [.isBackgroundProcessingRestricted] before showing the dialog.
+ *
+ * @param context The `Context` required to check the system settings
+ * @param fragment The `Fragment` where the dialog should be shown
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
+ fun showRestrictedBackgroundProcessingWarningDialog(
+ context: Context,
+ fragment: Fragment
+ ): Boolean {
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isBackgroundProcessingRestricted(
+ context
+ )
+ ) {
+ val fragmentManager = fragment.fragmentManager
+ Validate.notNull(fragmentManager)
+ val dialog = BackgroundProcessingRestrictionWarningDialog()
+ dialog.setTargetFragment(fragment, Constants.DIALOG_BACKGROUND_RESTRICTION_WARNING_CODE)
+ dialog.show(fragmentManager!!, "BACKGROUND_RESTRICTION_WARNING_DIALOG")
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [BackgroundProcessingRestrictionWarningDialog] when
+ * [.isBackgroundProcessingRestricted] is true.
+ *
+ * Checks [.isBackgroundProcessingRestricted] before showing the dialog.
+ *
+ * @param activity Required to show the dialog
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showRestrictedBackgroundProcessingWarningDialog(activity: Activity?): Boolean {
+
+ if (activity == null || activity.isFinishing) {
+ Log.w(
+ Constants.TAG,
+ "showRestrictedBackgroundProcessingWarningDialog: aborted, activity is null"
+ )
+ return false
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isBackgroundProcessingRestricted(
+ activity.applicationContext
+ )
+ ) {
+ BackgroundProcessingRestrictionWarningDialog.create(activity).show()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [ProblematicManufacturerWarningDialog] if [.isProblematicManufacturer] is true.
+ *
+ * This also checks if the user prefers not to see the dialog if he did not request guidance by himself.
+ *
+ * @param context The `Context` required to check the system settings
+ * @param fragment The `Fragment` where the dialog should be shown
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
+ * template.
+ * @param force `True` if the dialog should be shown no matter of the preferences state
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
+ fun showProblematicManufacturerDialog(
+ context: Context,
+ fragment: Fragment,
+ force: Boolean,
+ recipientEmail: String
+ ): Boolean {
+
+ val preferences = CustomPreferences(context)
+ if (isProblematicManufacturer && (force || !preferences.getWarningShown())) {
+ val fragmentManager = fragment.fragmentManager
+ Validate.notNull(fragmentManager)
+ val dialog = ProblematicManufacturerWarningDialog(recipientEmail)
+ dialog.setTargetFragment(
+ fragment,
+ Constants.DIALOG_PROBLEMATIC_MANUFACTURER_WARNING_CODE
+ )
+ dialog.show(fragmentManager!!, "PROBLEMATIC_MANUFACTURER_WARNING_DIALOG")
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [ProblematicManufacturerWarningDialog] if [.isProblematicManufacturer] is true.
+ *
+ * This also checks if the user prefers not to see the dialog if he did not request guidance by himself.
+ *
+ * @param activity Required to show the dialog
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
+ * template.
+ * @param force `True` if the dialog should be shown no matter of the preferences state
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showProblematicManufacturerDialog(
+ activity: Activity?,
+ force: Boolean,
+ recipientEmail: String
+ ): Boolean {
+
+ if (activity == null || activity.isFinishing) {
+ Log.w(Constants.TAG, "showProblematicManufacturerDialog: aborted, activity is null")
+ return false
+ }
+
+ val preferences = CustomPreferences(activity.applicationContext)
+ if (isProblematicManufacturer && (force || !preferences.getWarningShown())) {
+ ProblematicManufacturerWarningDialog.create(activity, recipientEmail).show()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Shows a [GnssDisabledWarningDialog] when [.isGpsEnabled] is true.
+ *
+ * @param context The `Context` required to check the system settings
+ * @param fragment The `Fragment` where the dialog should be shown
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
+ fun showGnssWarningDialog(context: Context, fragment: Fragment): Boolean {
+
+ if (isGnssEnabled(context)) {
+ return false
+ }
+
+ val fragmentManager = fragment.fragmentManager
+ Validate.notNull(fragmentManager)
+ val dialog = GnssDisabledWarningDialog()
+ dialog.setTargetFragment(fragment, Constants.DIALOG_GPS_DISABLED_WARNING_CODE)
+ dialog.show(fragmentManager!!, "GPS_DISABLED_WARNING_DIALOG")
+ return true
+ }
+
+ /**
+ * Shows a [GnssDisabledWarningDialog] when [.isGpsEnabled] is true.
+ *
+ * @param activity Required to show the dialog
+ * @return `True` if the dialog is shown
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showGnssWarningDialog(activity: Activity?): Boolean {
+
+ if (activity == null || activity.isFinishing) {
+ Log.w(Constants.TAG, "showGnssWarningDialog: aborted, activity is null")
+ return false
+ }
+ val context = activity.applicationContext
+ if (isGnssEnabled(context)) {
+ return false
+ }
+
+ GnssDisabledWarningDialog.create(activity).show()
+ return true
+ }
+
+ /**
+ * Shows a [NoGuidanceNeededDialog].
+ *
+ * @param fragment The `Fragment` where the dialog should be shown
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
+ * template.
+ */
+ @JvmStatic
+ @Deprecated("Alternative implementation recommended as this one is currently not being tested.")
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showNoGuidanceNeededDialog(fragment: Fragment, recipientEmail: String) {
+
+ val fragmentManager = fragment.fragmentManager
+ Validate.notNull(fragmentManager)
+ val dialog = NoGuidanceNeededDialog(recipientEmail)
+ dialog.setTargetFragment(fragment, Constants.DIALOG_NO_GUIDANCE_NEEDED_DIALOG_CODE)
+ dialog.show(fragmentManager!!, "NO_GUIDANCE_NEEDED_DIALOG")
+ }
+
+ /**
+ * Shows a [NoGuidanceNeededDialog].
+ *
+ * @param activity Required to show the dialog
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
+ * template.
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun showNoGuidanceNeededDialog(activity: Activity?, recipientEmail: String) {
+
+ if (activity == null || activity.isFinishing) {
+ Log.w(Constants.TAG, "showNoGuidanceNeededDialog: aborted, activity is null")
+ return
+ }
+
+ NoGuidanceNeededDialog.create(activity, recipientEmail).show()
+ }
+
+ /**
+ * Generates an `Intent` which can be used to open an e-mail app where a new e-mail draft is opened
+ * containing a template for a feedback email.
+ *
+ * @param context The `Context` required to get the app version for the template's subject field
+ * @param extraText The title for the text area where the user can write his message. E.g. "Your message"
+ * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated
+ * template.
+ * @return The intent
+ */
+ @JvmStatic
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun generateFeedbackEmailIntent(
+ context: Context,
+ extraText: String,
+ recipientEmail: String
+ ): Intent {
+
+ val appVersion = getAppVersion(context)
+ val appAndDeviceInfo = prepareAppAndDeviceInformation(context, appVersion)
+ val mailSubject =
+ (context.getString(R.string.app_name) + " " + context.getString(R.string.feedback_email_subject) + " (" + appVersion + "-"
+ + Build.VERSION.SDK_INT + ")")
+
+ val emailIntent = Intent(Intent.ACTION_SEND)
+ emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(recipientEmail))
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, mailSubject)
+ emailIntent.type = "plain/text"
+ emailIntent.putExtra(Intent.EXTRA_TEXT, "$appAndDeviceInfo\n---\n$extraText:\n\n\n")
+ return emailIntent
+ }
+
+ /**
+ * Load the app version as string.
+ *
+ * @param context The `Context` required to get the app version
+ * @return The app version as string
+ */
+ private fun getAppVersion(context: Context): String {
+
+ val packageManager = context.packageManager
+ return try {
+ packageManager.getPackageInfo(context.packageName, 0).versionName
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(Constants.TAG, "App version could not be identified: $e")
+ "N/A"
+ }
+ }
+
+ /**
+ * This function loads information about the app and device.
+ *
+ * @param context The `Context` required to get the text template
+ * @param appVersion The app version as string
+ * @return Device and app information
+ */
+ private fun prepareAppAndDeviceInformation(context: Context, appVersion: String): String {
+
+ // Replace app version, commit id and device info dynamically
+ return (context.getString(R.string.feedback_version_text) + ": " + appVersion + "\n"
+ + context.getString(R.string.feedback_device_text) + ": " + Build.MANUFACTURER + ", "
+ + Build.MODEL + " (" + Build.DEVICE + ")\n" + context.getString(R.string.feedback_android_text) + ": "
+ + Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")")
+ }
+
+ /**
+ * Dismisses all [EnergySettingDialog]s.
+ *
+ * You can use this in your `Activity#onPause()` method to hide all dialogs from this library
+ * as the user might have changed his settings while the app is paused so you should recheck the settings
+ * in `Activity#onResume()` and only show the dialogs required then.
+ *
+ * @param fragmentManager The `FragmentManager` required to search for open dialogs.
+ */
+ @JvmStatic
+ @Deprecated("Only works with deprecated DialogFragment implementations. Should not be needed for MaterialDialogs.")
+ @Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
+ fun dismissAllDialogs(fragmentManager: FragmentManager) {
+
+ val fragments = fragmentManager.fragments
+ for (fragment in fragments) {
+ if (fragment is EnergySettingDialog) {
+ fragment.dismissAllowingStateLoss()
+ }
+ val childFragmentManager = fragment.childFragmentManager
+ @Suppress("DEPRECATION") // Ok as this is called inside the method marked as deprecated
+ dismissAllDialogs(childFragmentManager)
+ }
+ }
+}