From 6d015a193544902ece2f0cc8beb5c6015332cbd5 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Wed, 27 Oct 2021 13:44:31 -0300 Subject: [PATCH] Add do-not-kill-lib from Briar and show its fragment whenever needed --- build.gradle | 8 +- dont-kill-me-lib/.gitignore | 1 + dont-kill-me-lib/build.gradle | 32 ++++ dont-kill-me-lib/consumer-rules.pro | 0 dont-kill-me-lib/proguard-rules.pro | 21 +++ dont-kill-me-lib/src/main/AndroidManifest.xml | 13 ++ .../AbstractDoNotKillMeFragment.java | 113 ++++++++++++ .../AbstractDozeWatchdogImpl.java | 49 ++++++ .../android/dontkillmelib/DozeHelper.java | 7 + .../android/dontkillmelib/DozeHelperImpl.java | 16 ++ .../android/dontkillmelib/DozeView.java | 57 ++++++ .../dontkillmelib/HuaweiAppLaunchView.java | 72 ++++++++ .../HuaweiProtectedAppsView.java | 74 ++++++++ .../android/dontkillmelib/PowerUtils.java | 60 +++++++ .../android/dontkillmelib/PowerView.java | 162 ++++++++++++++++++ .../android/dontkillmelib/XiaomiView.java | 67 ++++++++ .../src/main/res/drawable/ic_check_white.xml | 9 + .../res/drawable/ic_help_outline_white.xml | 9 + .../main/res/layout/fragment_dont_kill_me.xml | 77 +++++++++ .../src/main/res/layout/power_view.xml | 60 +++++++ .../src/main/res/values/strings.xml | 24 +++ .../src/main/res/values/styles.xml | 12 ++ mailbox-android/build.gradle | 12 +- .../briarproject/mailbox/android/AppModule.kt | 14 ++ .../mailbox/android/DoNotKillMeFragment.kt | 15 ++ .../mailbox/android/MailboxViewModel.kt | 20 ++- .../mailbox/android/MainActivity.kt | 77 ++++----- .../mailbox/android/MainFragment.kt | 81 +++++++++ .../mailbox/core/AndroidEagerSingletons.kt | 2 + .../mailbox/core/system/DozeWatchdog.kt | 7 + .../src/main/res/layout/activity_main.xml | 57 +----- .../src/main/res/layout/fragment_main.xml | 52 ++++++ .../src/main/res/values/strings.xml | 20 ++- settings.gradle | 3 + 34 files changed, 1191 insertions(+), 112 deletions(-) create mode 100644 dont-kill-me-lib/.gitignore create mode 100644 dont-kill-me-lib/build.gradle create mode 100644 dont-kill-me-lib/consumer-rules.pro create mode 100644 dont-kill-me-lib/proguard-rules.pro create mode 100644 dont-kill-me-lib/src/main/AndroidManifest.xml create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDoNotKillMeFragment.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDozeWatchdogImpl.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelper.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelperImpl.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeView.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiAppLaunchView.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiProtectedAppsView.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerUtils.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerView.java create mode 100644 dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/XiaomiView.java create mode 100644 dont-kill-me-lib/src/main/res/drawable/ic_check_white.xml create mode 100644 dont-kill-me-lib/src/main/res/drawable/ic_help_outline_white.xml create mode 100644 dont-kill-me-lib/src/main/res/layout/fragment_dont_kill_me.xml create mode 100644 dont-kill-me-lib/src/main/res/layout/power_view.xml create mode 100644 dont-kill-me-lib/src/main/res/values/strings.xml create mode 100644 dont-kill-me-lib/src/main/res/values/styles.xml create mode 100644 mailbox-android/src/main/java/org/briarproject/mailbox/android/DoNotKillMeFragment.kt create mode 100644 mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt create mode 100644 mailbox-android/src/main/java/org/briarproject/mailbox/core/system/DozeWatchdog.kt create mode 100644 mailbox-android/src/main/res/layout/fragment_main.xml diff --git a/build.gradle b/build.gradle index 85ad63cd..69505930 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,21 @@ buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.5.31' ext.hilt_version = '2.38.1' ext.tor_version = '0.3.5.15' ext.obfs4_version = '0.0.12-dev-40245c4a' ext.junit_version = '5.7.2' ext.mockk_version = '1.10.4' ext.ktlint_plugin_version = '10.1.0' + + ext.androidx_fragment_version = '1.3.6' + ext.androidx_constraintlayout_version = '2.1.1' + ext.google_material_version = '1.4.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/dont-kill-me-lib/.gitignore b/dont-kill-me-lib/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/dont-kill-me-lib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/dont-kill-me-lib/build.gradle b/dont-kill-me-lib/build.gradle new file mode 100644 index 00000000..9c180a0b --- /dev/null +++ b/dont-kill-me-lib/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + + vectorDrawables.useSupportLibrary = true + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "androidx.fragment:fragment:$androidx_fragment_version" + implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version" + implementation "com.google.android.material:material:$google_material_version" +} diff --git a/dont-kill-me-lib/consumer-rules.pro b/dont-kill-me-lib/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/dont-kill-me-lib/proguard-rules.pro b/dont-kill-me-lib/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/dont-kill-me-lib/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/dont-kill-me-lib/src/main/AndroidManifest.xml b/dont-kill-me-lib/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c186d0bf --- /dev/null +++ b/dont-kill-me-lib/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.briarproject.android.dontkillmelib"> + + <uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> + + <queries> + <package android:name="com.huawei.systemmanager" /> + <package android:name="com.huawei.powergenie" /> + <package android:name="com.evenwell.PowerMonitor" /> + </queries> + +</manifest> diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDoNotKillMeFragment.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDoNotKillMeFragment.java new file mode 100644 index 00000000..77ebbceb --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDoNotKillMeFragment.java @@ -0,0 +1,113 @@ +package org.briarproject.android.dontkillmelib; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; + +import org.briarproject.android.dontkillmelib.PowerView.OnCheckedChangedListener; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static org.briarproject.android.dontkillmelib.PowerUtils.getDozeWhitelistingIntent; +import static org.briarproject.android.dontkillmelib.PowerUtils.showOnboardingDialog; + +public abstract class AbstractDoNotKillMeFragment extends Fragment + implements OnCheckedChangedListener, + ActivityResultCallback<ActivityResult> { + + private DozeView dozeView; + private HuaweiProtectedAppsView huaweiProtectedAppsView; + private HuaweiAppLaunchView huaweiAppLaunchView; + private XiaomiView xiaomiView; + private Button next; + private boolean secondAttempt = false; + private boolean buttonWasClicked = false; + + private final ActivityResultLauncher<Intent> dozeLauncher = + registerForActivityResult(new StartActivityForResult(), this); + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + requireActivity().setTitle(getString(R.string.setup_doze_title)); + setHasOptionsMenu(false); + View v = inflater.inflate(R.layout.fragment_dont_kill_me, container, + false); + dozeView = v.findViewById(R.id.dozeView); + dozeView.setOnCheckedChangedListener(this); + huaweiProtectedAppsView = v.findViewById(R.id.huaweiProtectedAppsView); + huaweiProtectedAppsView.setOnCheckedChangedListener(this); + huaweiAppLaunchView = v.findViewById(R.id.huaweiAppLaunchView); + huaweiAppLaunchView.setOnCheckedChangedListener(this); + xiaomiView = v.findViewById(R.id.xiaomiView); + xiaomiView.setOnCheckedChangedListener(this); + next = v.findViewById(R.id.next); + ProgressBar progressBar = v.findViewById(R.id.progress); + + dozeView.setOnButtonClickListener(this::askForDozeWhitelisting); + next.setOnClickListener(view -> { + buttonWasClicked = true; + next.setVisibility(INVISIBLE); + progressBar.setVisibility(VISIBLE); + onButtonClicked(); + }); + + // restore UI state if button was clicked already + buttonWasClicked = savedInstanceState != null && + savedInstanceState.getBoolean("buttonWasClicked", false); + if (buttonWasClicked) { + next.setVisibility(INVISIBLE); + progressBar.setVisibility(VISIBLE); + } + + return v; + } + + protected abstract void onButtonClicked(); + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("buttonWasClicked", buttonWasClicked); + } + + @Override + public void onActivityResult(ActivityResult result) { + // we allow the user to proceed after also denying the second attempt + if (!dozeView.needsToBeShown() || secondAttempt) { + dozeView.setChecked(true); + } else if (getContext() != null) { + secondAttempt = true; + String s = getString(R.string.setup_doze_explanation); + showOnboardingDialog(getContext(), s); + } + } + + @Override + public void onCheckedChanged() { + next.setEnabled(dozeView.isChecked() && + huaweiProtectedAppsView.isChecked() && + huaweiAppLaunchView.isChecked() && + xiaomiView.isChecked()); + } + + @SuppressLint("BatteryLife") + private void askForDozeWhitelisting() { + if (getContext() == null) return; + dozeLauncher.launch(getDozeWhitelistingIntent(getContext())); + } +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDozeWatchdogImpl.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDozeWatchdogImpl.java new file mode 100644 index 00000000..57240726 --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/AbstractDozeWatchdogImpl.java @@ -0,0 +1,49 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PowerManager; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED; + +public abstract class AbstractDozeWatchdogImpl { + + private final Context appContext; + private final AtomicBoolean dozed = new AtomicBoolean(false); + private final BroadcastReceiver receiver = new DozeBroadcastReceiver(); + + public AbstractDozeWatchdogImpl(Context appContext) { + this.appContext = appContext; + } + + public boolean getAndResetDozeFlag() { + return dozed.getAndSet(false); + } + + public void startService() { + if (SDK_INT < 23) return; + IntentFilter filter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED); + appContext.registerReceiver(receiver, filter); + } + + public void stopService() { + if (SDK_INT < 23) return; + appContext.unregisterReceiver(receiver); + } + + private class DozeBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (SDK_INT < 23) return; + PowerManager pm = + (PowerManager) appContext.getSystemService(POWER_SERVICE); + if (pm.isDeviceIdleMode()) dozed.set(true); + } + } +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelper.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelper.java new file mode 100644 index 00000000..ea80f78b --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelper.java @@ -0,0 +1,7 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; + +public interface DozeHelper { + boolean needToShowDoNotKillMeFragment(Context context); +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelperImpl.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelperImpl.java new file mode 100644 index 00000000..7a284b70 --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeHelperImpl.java @@ -0,0 +1,16 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; + +import static org.briarproject.android.dontkillmelib.PowerUtils.needsDozeWhitelisting; + +public class DozeHelperImpl implements DozeHelper { + @Override + public boolean needToShowDoNotKillMeFragment(Context context) { + Context appContext = context.getApplicationContext(); + return needsDozeWhitelisting(appContext) || + HuaweiProtectedAppsView.needsToBeShown(appContext) || + HuaweiAppLaunchView.needsToBeShown(appContext) || + XiaomiView.isXiaomiOrRedmiDevice(); + } +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeView.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeView.java new file mode 100644 index 00000000..b9e87aea --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/DozeView.java @@ -0,0 +1,57 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +import static org.briarproject.android.dontkillmelib.PowerUtils.needsDozeWhitelisting; + +@UiThread +class DozeView extends PowerView { + + @Nullable + private Runnable onButtonClickListener; + + public DozeView(Context context) { + this(context, null); + } + + public DozeView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DozeView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + setText(R.string.setup_doze_intro); + setButtonText(R.string.setup_doze_button); + } + + @Override + public boolean needsToBeShown() { + return needsToBeShown(getContext()); + } + + public static boolean needsToBeShown(Context context) { + return needsDozeWhitelisting(context); + } + + @Override + protected int getHelpText() { + return R.string.setup_doze_explanation; + } + + @Override + protected void onButtonClick() { + if (onButtonClickListener == null) throw new IllegalStateException(); + onButtonClickListener.run(); + } + + public void setOnButtonClickListener(@NonNull Runnable runnable) { + onButtonClickListener = runnable; + } + +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiAppLaunchView.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiAppLaunchView.java new file mode 100644 index 00000000..7a84c52a --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiAppLaunchView.java @@ -0,0 +1,72 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.util.AttributeSet; + +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; + +import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; +import static android.os.Build.VERSION.SDK_INT; + +@UiThread +class HuaweiAppLaunchView extends PowerView { + + private final static String PACKAGE_NAME = "com.huawei.systemmanager"; + private final static String CLASS_NAME = + PACKAGE_NAME + ".power.ui.HwPowerManagerActivity"; + + public HuaweiAppLaunchView(Context context) { + this(context, null); + } + + public HuaweiAppLaunchView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public HuaweiAppLaunchView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + setText(R.string.setup_huawei_app_launch_text); + setButtonText(R.string.setup_huawei_app_launch_button); + } + + @Override + public boolean needsToBeShown() { + return needsToBeShown(getContext()); + } + + public static boolean needsToBeShown(Context context) { + // "App launch" was introduced in EMUI 8 (Android 8.0) + if (SDK_INT < 26) return false; + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(), + MATCH_DEFAULT_ONLY); + return !resolveInfos.isEmpty(); + } + + @Override + @StringRes + protected int getHelpText() { + return R.string.setup_huawei_app_launch_help; + } + + @Override + protected void onButtonClick() { + getContext().startActivity(getIntent()); + setChecked(true); + } + + private static Intent getIntent() { + Intent intent = new Intent(); + intent.setClassName(PACKAGE_NAME, CLASS_NAME); + return intent; + } + +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiProtectedAppsView.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiProtectedAppsView.java new file mode 100644 index 00000000..3d63bed3 --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/HuaweiProtectedAppsView.java @@ -0,0 +1,74 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.util.AttributeSet; + +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; + +import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; +import static android.os.Build.VERSION.SDK_INT; + +@UiThread +class HuaweiProtectedAppsView extends PowerView { + + private final static String PACKAGE_NAME = "com.huawei.systemmanager"; + private final static String CLASS_NAME = + PACKAGE_NAME + ".optimize.process.ProtectActivity"; + + public HuaweiProtectedAppsView(Context context) { + this(context, null); + } + + public HuaweiProtectedAppsView(Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public HuaweiProtectedAppsView(Context context, + @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + setText(R.string.setup_huawei_text); + setButtonText(R.string.setup_huawei_button); + } + + @Override + public boolean needsToBeShown() { + return needsToBeShown(getContext()); + } + + public static boolean needsToBeShown(Context context) { + // "Protected apps" no longer exists on Huawei EMUI 5.0 (Android 7.0) + if (SDK_INT >= 24) return false; + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(), + MATCH_DEFAULT_ONLY); + return !resolveInfos.isEmpty(); + } + + @Override + @StringRes + protected int getHelpText() { + return R.string.setup_huawei_help; + } + + @Override + protected void onButtonClick() { + getContext().startActivity(getIntent()); + setChecked(true); + } + + private static Intent getIntent() { + Intent intent = new Intent(); + intent.setClassName(PACKAGE_NAME, CLASS_NAME); + return intent; + } + +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerUtils.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerUtils.java new file mode 100644 index 00000000..8b130f6a --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerUtils.java @@ -0,0 +1,60 @@ +package org.briarproject.android.dontkillmelib; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.PowerManager; + +import java.io.IOException; +import java.util.Scanner; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; +import static java.lang.Runtime.getRuntime; + +public class PowerUtils { + + public static boolean needsDozeWhitelisting(Context ctx) { + if (SDK_INT < 23) return false; + PowerManager pm = (PowerManager) ctx.getSystemService(POWER_SERVICE); + String packageName = ctx.getPackageName(); + if (pm == null) throw new AssertionError(); + return !pm.isIgnoringBatteryOptimizations(packageName); + } + + @TargetApi(23) + @SuppressLint("BatteryLife") + public static Intent getDozeWhitelistingIntent(Context ctx) { + Intent i = new Intent(); + i.setAction(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + i.setData(Uri.parse("package:" + ctx.getPackageName())); + return i; + } + + static void showOnboardingDialog(Context ctx, String text) { + new AlertDialog.Builder(ctx, R.style.OnboardingDialogTheme) + .setMessage(text) + .setNeutralButton(R.string.got_it, + (dialog, which) -> dialog.cancel()) + .show(); + } + + @Nullable + static String getSystemProperty(String propName) { + try { + Process p = getRuntime().exec("getprop " + propName); + Scanner s = new Scanner(p.getInputStream()); + String line = s.nextLine(); + s.close(); + return line; + } catch (SecurityException | IOException e) { + return null; + } + } +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerView.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerView.java new file mode 100644 index 00000000..8144e7cb --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/PowerView.java @@ -0,0 +1,162 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; +import androidx.constraintlayout.widget.ConstraintLayout; + +import static android.content.Context.LAYOUT_INFLATER_SERVICE; +import static org.briarproject.android.dontkillmelib.PowerUtils.showOnboardingDialog; + +@UiThread +abstract class PowerView extends ConstraintLayout { + + private final TextView textView; + private final ImageView checkImage; + private final Button button; + + private boolean checked = false; + + @Nullable + private OnCheckedChangedListener onCheckedChangedListener; + + public PowerView(Context context) { + this(context, null); + } + + public PowerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public PowerView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(LAYOUT_INFLATER_SERVICE); + View v = inflater.inflate(R.layout.power_view, this, true); + + textView = v.findViewById(R.id.textView); + checkImage = v.findViewById(R.id.checkImage); + button = v.findViewById(R.id.button); + button.setOnClickListener(view -> onButtonClick()); + ImageButton helpButton = v.findViewById(R.id.helpButton); + helpButton.setOnClickListener(view -> onHelpButtonClick()); + + // we need to manage the checkImage state ourselves, because automatic + // state saving is done based on the view's ID and there can be + // multiple ImageViews with the same ID in the view hierarchy + setSaveFromParentEnabled(true); + + if (!isInEditMode() && !needsToBeShown()) { + setVisibility(GONE); + } + } + + @Nullable + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.value = new boolean[] {checked}; + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setChecked(ss.value[0]); // also calls listener + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public abstract boolean needsToBeShown(); + + public void setChecked(boolean checked) { + this.checked = checked; + if (checked) { + checkImage.setVisibility(VISIBLE); + } else { + checkImage.setVisibility(INVISIBLE); + } + if (onCheckedChangedListener != null) { + onCheckedChangedListener.onCheckedChanged(); + } + } + + public boolean isChecked() { + return getVisibility() == GONE || checked; + } + + public void setOnCheckedChangedListener(@NonNull + OnCheckedChangedListener onCheckedChangedListener) { + this.onCheckedChangedListener = onCheckedChangedListener; + } + + @StringRes + protected abstract int getHelpText(); + + protected void setText(@StringRes int res) { + textView.setText(res); + } + + protected void setButtonText(@StringRes int res) { + button.setText(res); + } + + protected abstract void onButtonClick(); + + private void onHelpButtonClick() { + showOnboardingDialog(getContext(), + getContext().getString(getHelpText())); + } + + private static class SavedState extends BaseSavedState { + private boolean[] value = {false}; + + private SavedState(@Nullable Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + in.readBooleanArray(value); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeBooleanArray(value); + } + + static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + interface OnCheckedChangedListener { + void onCheckedChanged(); + } + +} diff --git a/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/XiaomiView.java b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/XiaomiView.java new file mode 100644 index 00000000..0233b1ad --- /dev/null +++ b/dont-kill-me-lib/src/main/java/org/briarproject/android/dontkillmelib/XiaomiView.java @@ -0,0 +1,67 @@ +package org.briarproject.android.dontkillmelib; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; + +import static android.os.Build.BRAND; +import static org.briarproject.android.dontkillmelib.PowerUtils.getSystemProperty; +import static org.briarproject.android.dontkillmelib.PowerUtils.showOnboardingDialog; + +@UiThread +class XiaomiView extends PowerView { + + public XiaomiView(Context context) { + this(context, null); + } + + public XiaomiView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public XiaomiView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + setText(R.string.setup_xiaomi_text); + setButtonText(R.string.setup_xiaomi_button); + } + + @Override + public boolean needsToBeShown() { + return isXiaomiOrRedmiDevice(); + } + + public static boolean isXiaomiOrRedmiDevice() { + return "Xiaomi".equalsIgnoreCase(BRAND) || + "Redmi".equalsIgnoreCase(BRAND); + } + + @Override + @StringRes + protected int getHelpText() { + return R.string.setup_xiaomi_help; + } + + @Override + protected void onButtonClick() { + int bodyRes = isMiuiTenOrLater() + ? R.string.setup_xiaomi_dialog_body_new + : R.string.setup_xiaomi_dialog_body_old; + showOnboardingDialog(getContext(), getContext().getString(bodyRes)); + setChecked(true); + } + + private boolean isMiuiTenOrLater() { + String version = getSystemProperty("ro.miui.ui.version.name"); + if (version == null || version.equals("")) return false; + version = version.replaceAll("[^\\d]", ""); + try { + return Integer.parseInt(version) >= 10; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/dont-kill-me-lib/src/main/res/drawable/ic_check_white.xml b/dont-kill-me-lib/src/main/res/drawable/ic_check_white.xml new file mode 100644 index 00000000..93a0bc27 --- /dev/null +++ b/dont-kill-me-lib/src/main/res/drawable/ic_check_white.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" /> +</vector> diff --git a/dont-kill-me-lib/src/main/res/drawable/ic_help_outline_white.xml b/dont-kill-me-lib/src/main/res/drawable/ic_help_outline_white.xml new file mode 100644 index 00000000..8b07d94a --- /dev/null +++ b/dont-kill-me-lib/src/main/res/drawable/ic_help_outline_white.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" /> +</vector> diff --git a/dont-kill-me-lib/src/main/res/layout/fragment_dont_kill_me.xml b/dont-kill-me-lib/src/main/res/layout/fragment_dont_kill_me.xml new file mode 100644 index 00000000..248b6af2 --- /dev/null +++ b/dont-kill-me-lib/src/main/res/layout/fragment_dont_kill_me.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp"> + + <org.briarproject.android.dontkillmelib.DozeView + android:id="@+id/dozeView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <org.briarproject.android.dontkillmelib.HuaweiProtectedAppsView + android:id="@+id/huaweiProtectedAppsView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/dozeView" /> + + <org.briarproject.android.dontkillmelib.HuaweiAppLaunchView + android:id="@+id/huaweiAppLaunchView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/huaweiProtectedAppsView" /> + + <org.briarproject.android.dontkillmelib.XiaomiView + android:id="@+id/xiaomiView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/huaweiAppLaunchView" /> + + <Button + android:id="@+id/next" + style="@style/DoNotKillMeButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:enabled="false" + android:text="@string/create_account_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/xiaomiView" + app:layout_constraintVertical_bias="1.0" + tools:enabled="true" /> + + <ProgressBar + android:id="@+id/progress" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="@+id/next" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/next" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</ScrollView> diff --git a/dont-kill-me-lib/src/main/res/layout/power_view.xml b/dont-kill-me-lib/src/main/res/layout/power_view.xml new file mode 100644 index 00000000..f78feff3 --- /dev/null +++ b/dont-kill-me-lib/src/main/res/layout/power_view.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <TextView + android:id="@+id/textView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" + android:textSize="16sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/setup_huawei_text" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/checkImage" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_margin="8dp" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="@+id/button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/button" + app:srcCompat="@drawable/ic_check_white" + app:tint="?attr/colorControlNormal" + tools:ignore="ContentDescription" /> + + <Button + android:id="@+id/button" + style="@style/DoNotKillMeButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:layout_constraintEnd_toStartOf="@+id/helpButton" + app:layout_constraintStart_toEndOf="@+id/checkImage" + app:layout_constraintTop_toBottomOf="@+id/textView" + tools:text="@string/setup_huawei_button" /> + + <ImageButton + android:id="@+id/helpButton" + style="@style/HelpButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/help" + app:layout_constraintBottom_toBottomOf="@+id/button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/button" + app:srcCompat="@drawable/ic_help_outline_white" /> + +</merge> diff --git a/dont-kill-me-lib/src/main/res/values/strings.xml b/dont-kill-me-lib/src/main/res/values/strings.xml new file mode 100644 index 00000000..262102af --- /dev/null +++ b/dont-kill-me-lib/src/main/res/values/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> + + <string name="setup_doze_title">Background Connections</string> + <string name="setup_doze_intro">To work properly, this app needs to run in the background.</string> + <string name="setup_doze_explanation">Please disable battery optimizations so this app can run in the background.</string> + <string name="setup_doze_button">Allow Connections</string> + <string name="create_account_button">Continue</string> + + <string name="setup_huawei_text">Please tap the button below and make sure this app is protected in the \"Protected Apps\" screen.</string> + <string name="setup_huawei_button">Protect this app</string> + <string name="setup_huawei_help">If this app is not added to the protected apps list, it will be unable to run in the background.</string> + <string name="setup_huawei_app_launch_text">Please tap the button below, open the \"App launch\" screen and make sure this app is set to \"Manage manually\".</string> + <string name="setup_huawei_app_launch_button">Open Battery Settings</string> + <string name="setup_huawei_app_launch_help">If this app is not set to \"Manage manually\" in the \"App launch\" screen, it will not be able to run in the background.</string> + <string name="setup_xiaomi_text">To run in the background, this app needs to be locked to the recent apps list.</string> + <string name="setup_xiaomi_button">Protect this app</string> + <string name="setup_xiaomi_help">If this app is not locked to the recent apps list, it will be unable to run in the background.</string> + <string name="setup_xiaomi_dialog_body_old">1. Open the recent apps list (also called the app switcher)\n\n2. Swipe down on the image of this app to show the padlock icon\n\n3. If the padlock is not locked, tap to lock it</string> + <string name="setup_xiaomi_dialog_body_new">1. Open the recent apps list (also called the app switcher)\n\n2. Press and hold the image of this app until the padlock button appears\n\n3. If the padlock is not locked, tap to lock it</string> + + <string name="got_it">Got it</string> + <string name="help">Help</string> +</resources> diff --git a/dont-kill-me-lib/src/main/res/values/styles.xml b/dont-kill-me-lib/src/main/res/values/styles.xml new file mode 100644 index 00000000..b608b8fd --- /dev/null +++ b/dont-kill-me-lib/src/main/res/values/styles.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="OnboardingDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.MinWidth" /> + + <style name="DoNotKillMeButton" parent="Widget.AppCompat.Button.Colored" /> + + <style name="HelpButton" parent="Widget.AppCompat.Button.Borderless"> + <item name="android:tint">#418cd8</item> + </style> + +</resources> diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle index e4868ed5..7011bc02 100644 --- a/mailbox-android/build.gradle +++ b/mailbox-android/build.gradle @@ -9,8 +9,8 @@ plugins { } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdkVersion 31 + buildToolsVersion "31.0.0" defaultConfig { applicationId "org.briarproject.mailbox" @@ -51,20 +51,22 @@ configurations { dependencies { implementation project(path: ':mailbox-core', configuration: 'default') + implementation project(path: ':dont-kill-me-lib') implementation 'com.github.tony19:logback-android:2.0.0' implementation 'androidx.appcompat:appcompat:1.3.1' implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.fragment:fragment-ktx:1.3.6" + implementation "androidx.fragment:fragment-ktx:$androidx_fragment_version" - def lifecycle_version = "2.4.0-alpha03" + def lifecycle_version = "2.4.0-rc01" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" - implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version" + implementation "com.google.android.material:material:$google_material_version" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt index 1e1469a6..0b2c3252 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt @@ -6,9 +6,13 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.briarproject.android.dontkillmelib.DozeHelper +import org.briarproject.android.dontkillmelib.DozeHelperImpl import org.briarproject.mailbox.core.CoreModule import org.briarproject.mailbox.core.db.DatabaseConfig import org.briarproject.mailbox.core.files.FileProvider +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.system.DozeWatchdog import java.io.File import javax.inject.Singleton @@ -38,4 +42,14 @@ internal class AppModule { override fun getFolder(folderId: String) = File(folderRoot, folderId).also { it.mkdirs() } override fun getFile(folderId: String, fileId: String) = File(getFolder(folderId), fileId) } + + @Singleton + @Provides + fun provideDozeWatchdog(app: Application, lifecycleManager: LifecycleManager): DozeWatchdog { + return DozeWatchdog(app).also { lifecycleManager.registerService(it) } + } + + @Singleton + @Provides + fun provideDozeHelper(): DozeHelper = DozeHelperImpl() } diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/DoNotKillMeFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/DoNotKillMeFragment.kt new file mode 100644 index 00000000..9adda59f --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/DoNotKillMeFragment.kt @@ -0,0 +1,15 @@ +package org.briarproject.mailbox.android + +import androidx.fragment.app.activityViewModels +import dagger.hilt.android.AndroidEntryPoint +import org.briarproject.android.dontkillmelib.AbstractDoNotKillMeFragment + +@AndroidEntryPoint +class DoNotKillMeFragment : AbstractDoNotKillMeFragment() { + + private val viewModel: MailboxViewModel by activityViewModels() + + override fun onButtonClicked() { + viewModel.onDoNotKillComplete() + } +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt index 3dd9c84d..015bbe05 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt @@ -1,27 +1,43 @@ package org.briarproject.mailbox.android import android.app.Application +import androidx.annotation.UiThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow +import org.briarproject.android.dontkillmelib.DozeHelper import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState +import org.briarproject.mailbox.core.system.DozeWatchdog import javax.inject.Inject @HiltViewModel class MailboxViewModel @Inject constructor( - app: Application, + private val app: Application, + private val dozeHelper: DozeHelper, + private val dozeWatchdog: DozeWatchdog, handle: SavedStateHandle, lifecycleManager: LifecycleManager, ) : AndroidViewModel(app) { + val needToShowDoNotKillMeFragment get() = dozeHelper.needToShowDoNotKillMeFragment(app) + + private val _doNotKillComplete = MutableLiveData<Boolean>() + val doNotKillComplete: LiveData<Boolean> = _doNotKillComplete + private val _text = handle.getLiveData("text", "Hello Mailbox") val text: LiveData<String> = _text val lifecycleState: StateFlow<LifecycleState> = lifecycleManager.lifecycleStateFlow + @UiThread + fun onDoNotKillComplete() { + _doNotKillComplete.value = true + } + fun startLifecycle() { MailboxService.startService(getApplication()) } @@ -30,6 +46,8 @@ class MailboxViewModel @Inject constructor( MailboxService.stopService(getApplication()) } + fun getAndResetDozeFlag() = dozeWatchdog.andResetDozeFlag + fun updateText(str: String) { _text.value = str } diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt index 679e69ac..9e926f3e 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainActivity.kt @@ -1,72 +1,57 @@ package org.briarproject.mailbox.android import android.os.Bundle -import android.widget.Button -import android.widget.TextView import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.fragment.app.Fragment import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch +import org.briarproject.android.dontkillmelib.PowerUtils.needsDozeWhitelisting import org.briarproject.mailbox.R -import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState @AndroidEntryPoint class MainActivity : AppCompatActivity() { private val viewModel: MailboxViewModel by viewModels() - private lateinit var statusTextView: TextView - private lateinit var startStopButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val textView = findViewById<TextView>(R.id.text) - val button = findViewById<Button>(R.id.button) - statusTextView = findViewById(R.id.statusTextView) - startStopButton = findViewById(R.id.startStopButton) - - button.setOnClickListener { - viewModel.updateText("Tested") + viewModel.doNotKillComplete.observe(this) { complete -> + if (complete) showFragment(MainFragment()) } - // Start a coroutine in the lifecycle scope - lifecycleScope.launch { - // repeatOnLifecycle launches the block in a new coroutine every time the - // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. - repeatOnLifecycle(Lifecycle.State.STARTED) { - // Trigger the flow and start listening for values. - // Note that this happens when lifecycle is STARTED and stops - // collecting when the lifecycle is STOPPED - viewModel.lifecycleState.collect { onLifecycleStateChanged(it) } + if (savedInstanceState == null) { + val f = if (viewModel.needToShowDoNotKillMeFragment) { + DoNotKillMeFragment() + } else { + MainFragment() } + showFragment(f) } - - viewModel.text.observe(this, { text -> - textView.text = text - }) } - private fun onLifecycleStateChanged(state: LifecycleState) = when (state) { - LifecycleState.STOPPED -> { - statusTextView.text = state.name - startStopButton.setText(R.string.start) - startStopButton.setOnClickListener { viewModel.startLifecycle() } - startStopButton.isEnabled = true - } - LifecycleState.RUNNING -> { - statusTextView.text = state.name - startStopButton.setText(R.string.stop) - startStopButton.setOnClickListener { viewModel.stopLifecycle() } - startStopButton.isEnabled = true - } - else -> { - statusTextView.text = state.name - startStopButton.isEnabled = false + override fun onResume() { + super.onResume() + if (needsDozeWhitelisting(this) && viewModel.getAndResetDozeFlag()) { + showDozeDialog() } } + + private fun showFragment(f: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, f) + .commitNow() + } + + private fun showDozeDialog() = AlertDialog.Builder(this) + .setMessage(R.string.warning_dozed) + .setPositiveButton(R.string.fix) { dialog, _ -> + showFragment(DoNotKillMeFragment()) + dialog.dismiss() + } + .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } + .show() + } diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt new file mode 100644 index 00000000..0f129e57 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt @@ -0,0 +1,81 @@ +package org.briarproject.mailbox.android + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.briarproject.mailbox.R +import org.briarproject.mailbox.core.lifecycle.LifecycleManager + +@AndroidEntryPoint +class MainFragment : Fragment() { + + private val viewModel: MailboxViewModel by activityViewModels() + private lateinit var statusTextView: TextView + private lateinit var startStopButton: Button + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.fragment_main, container, false) + } + + override fun onViewCreated(v: View, savedInstanceState: Bundle?) { + val textView = v.findViewById<TextView>(R.id.text) + val button = v.findViewById<Button>(R.id.button) + statusTextView = v.findViewById(R.id.statusTextView) + startStopButton = v.findViewById(R.id.startStopButton) + + button.setOnClickListener { + viewModel.updateText("Tested") + } + + // Start a coroutine in the lifecycle scope + lifecycleScope.launch { + // repeatOnLifecycle launches the block in a new coroutine every time the + // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Trigger the flow and start listening for values. + // Note that this happens when lifecycle is STARTED and stops + // collecting when the lifecycle is STOPPED + viewModel.lifecycleState.collect { onLifecycleStateChanged(it) } + } + } + + viewModel.text.observe(viewLifecycleOwner, { text -> + textView.text = text + }) + } + + private fun onLifecycleStateChanged(state: LifecycleManager.LifecycleState) = when (state) { + LifecycleManager.LifecycleState.STOPPED -> { + statusTextView.text = state.name + startStopButton.setText(R.string.start) + startStopButton.setOnClickListener { viewModel.startLifecycle() } + startStopButton.isEnabled = true + } + LifecycleManager.LifecycleState.RUNNING -> { + statusTextView.text = state.name + startStopButton.setText(R.string.stop) + startStopButton.setOnClickListener { viewModel.stopLifecycle() } + startStopButton.isEnabled = true + } + else -> { + statusTextView.text = state.name + startStopButton.isEnabled = false + } + } + +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt index d477e73e..d07cae6f 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/AndroidEagerSingletons.kt @@ -1,6 +1,7 @@ package org.briarproject.mailbox.core import org.briarproject.mailbox.core.system.AndroidTaskScheduler +import org.briarproject.mailbox.core.system.DozeWatchdog import org.briarproject.mailbox.core.tor.AndroidNetworkManager import org.briarproject.mailbox.core.tor.TorPlugin import javax.inject.Inject @@ -10,4 +11,5 @@ internal class AndroidEagerSingletons @Inject constructor( val androidTaskScheduler: AndroidTaskScheduler, val androidNetworkManager: AndroidNetworkManager, val androidTorPlugin: TorPlugin, + val dozeWatchdog: DozeWatchdog, ) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/DozeWatchdog.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/DozeWatchdog.kt new file mode 100644 index 00000000..34754d75 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/DozeWatchdog.kt @@ -0,0 +1,7 @@ +package org.briarproject.mailbox.core.system + +import android.content.Context +import org.briarproject.android.dontkillmelib.AbstractDozeWatchdogImpl +import org.briarproject.mailbox.core.lifecycle.Service + +class DozeWatchdog(appContext: Context) : AbstractDozeWatchdogImpl(appContext), Service diff --git a/mailbox-android/src/main/res/layout/activity_main.xml b/mailbox-android/src/main/res/layout/activity_main.xml index 26cce69f..16a344d6 100644 --- a/mailbox-android/src/main/res/layout/activity_main.xml +++ b/mailbox-android/src/main/res/layout/activity_main.xml @@ -1,52 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".android.MainActivity"> - - <TextView - android:id="@+id/text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Hello World!" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toRightOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <Button - android:id="@+id/button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:text="Test" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/text" /> - - <TextView - android:id="@+id/statusTextView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:gravity="center" - app:layout_constraintBottom_toTopOf="@+id/startStopButton" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/button" - app:layout_constraintVertical_bias="1.0" - tools:text="STOPPED" /> - - <Button - android:id="@+id/startStopButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/start" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> - -</androidx.constraintlayout.widget.ConstraintLayout> +<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/fragmentContainer" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".android.MainActivity" /> diff --git a/mailbox-android/src/main/res/layout/fragment_main.xml b/mailbox-android/src/main/res/layout/fragment_main.xml new file mode 100644 index 00000000..db4d9c81 --- /dev/null +++ b/mailbox-android/src/main/res/layout/fragment_main.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".android.MainActivity"> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Hello World!" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Test" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/text" /> + + <TextView + android:id="@+id/statusTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:gravity="center" + app:layout_constraintBottom_toTopOf="@+id/startStopButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button" + app:layout_constraintVertical_bias="1.0" + tools:text="STOPPED" /> + + <Button + android:id="@+id/startStopButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:text="@string/start" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml index 580b932a..8f99672c 100644 --- a/mailbox-android/src/main/res/values/strings.xml +++ b/mailbox-android/src/main/res/values/strings.xml @@ -1,8 +1,14 @@ <resources> - <string name="app_name">Briar Mailbox</string> - <string name="notification_channel_name">Briar Mailbox Channel</string> - <string name="notification_mailbox_title">Briar Mailbox running</string> - <string name="notification_mailbox_content">Waiting for messages…</string> - <string name="start">Start mailbox</string> - <string name="stop">Stop mailbox</string> -</resources> \ No newline at end of file + <string name="app_name">Briar Mailbox</string> + <string name="notification_channel_name">Briar Mailbox Channel</string> + <string name="notification_mailbox_title">Briar Mailbox running</string> + <string name="notification_mailbox_content">Waiting for messages…</string> + <string name="start">Start mailbox</string> + <string name="stop">Stop mailbox</string> + + <!-- TODO: We might want to copy string from don't kill me lib, + so translation memory can auto-translate most of them. --> + <string name="warning_dozed">Briar Mailbox was unable to run in the background</string> + <string name="fix">Fix</string> + <string name="cancel">Cancel</string> +</resources> diff --git a/settings.gradle b/settings.gradle index 8ef7b26e..24ab3119 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,6 @@ include ':mailbox-core' include ':mailbox-android' include ':mailbox-cli' +include ':mailbox-cli' + +include ':dont-kill-me-lib' -- GitLab