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) + } + } +}