diff --git a/build.gradle b/build.gradle
index 85ad63cd5782cd2b608a5946fbd5bb1487255ddb..6950593039b5b565412059ce848d5f059f58fd4e 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 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8
--- /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 0000000000000000000000000000000000000000..9c180a0b0e2c338381c2f15e28082c7d20b0317b
--- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/dont-kill-me-lib/proguard-rules.pro b/dont-kill-me-lib/proguard-rules.pro
new file mode 100644
index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce
--- /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 0000000000000000000000000000000000000000..c186d0bf284be39acf21c7b1ac285096800e9281
--- /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 0000000000000000000000000000000000000000..77ebbcebb870acef30f3bca562f2fb10bc1d865d
--- /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 0000000000000000000000000000000000000000..57240726f8dfd8f7f73a7b8b14bfe5820d905dbd
--- /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 0000000000000000000000000000000000000000..ea80f78bb26edbce7693bcae9569d34fe6ed17de
--- /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 0000000000000000000000000000000000000000..7a284b703ca2314bbd549008dd64d8da4fe72cef
--- /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 0000000000000000000000000000000000000000..b9e87aea78c243f72fc2c86dd1a711d3661ac7fb
--- /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 0000000000000000000000000000000000000000..7a84c52aa3caf13d29efeea1839a977da8130024
--- /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 0000000000000000000000000000000000000000..3d63bed36549fb5a93167ccd0fcebbee50354bc6
--- /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 0000000000000000000000000000000000000000..8b130f6ad8b672b65b5d1d6b49caf7bd9b50fea1
--- /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 0000000000000000000000000000000000000000..8144e7cbeef93fef95ab18dee5d4779f4eb5cce5
--- /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 0000000000000000000000000000000000000000..0233b1ad24a71b1a7f173fcc0c8c4dad8a9fdb70
--- /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 0000000000000000000000000000000000000000..93a0bc27d6fad5f0e66cbd9d8abdcfdd9a26eeda
--- /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 0000000000000000000000000000000000000000..8b07d94a06ba80a510902297aa485e2051c949b2
--- /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 0000000000000000000000000000000000000000..248b6af2a9c4573a62d33cd75c7815d7c1ebad9e
--- /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 0000000000000000000000000000000000000000..f78feff380f997b86430f09b5e892d2997d9be35
--- /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 0000000000000000000000000000000000000000..262102af05e166242aa9ac003de1df7c99d86789
--- /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 0000000000000000000000000000000000000000..b608b8fdf7fa11d51c5646355cb3700ca7b4b2ff
--- /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 e4868ed5433f1be1acc8ab0076ce6b49abb0870d..7011bc02761f4b08045966a662fb7f8bc0bab740 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 1e1469a6010e29cc602d9fa4c481cc3bd64c3040..0b2c3252676594e277629d24d4f47e8455f72cc1 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 0000000000000000000000000000000000000000..9adda59f21476695c14f1bc489a6d26b965d434a
--- /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 3dd9c84dcab8dd1997df7a3fe0d59c09bc8e9d15..015bbe053c8f303e057b0afa46e94bce10a7520b 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 679e69accb50dff7bd9c761633a790ffe3a4e461..9e926f3e2bf10e07f7a914d41a09fb7ce121bf06 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 0000000000000000000000000000000000000000..0f129e57150a358dc79df73ccfd7b709fde568c6
--- /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 d477e73e75133cacd5fa2e7373d7a6ccec4ab22e..d07cae6f72ed74958392e83bdc3c2b733cfb61ea 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 0000000000000000000000000000000000000000..34754d7571de6a261b5389b33241b67130e2d3fd
--- /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 26cce69f629360bc5c346f67b7cce544aed60c4a..16a344d6137121a9d0b483179277223268ce6e72 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 0000000000000000000000000000000000000000..db4d9c81771f55859d48f429bfebb3ab6a4c5488
--- /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 580b932af496ebd4ab45620215b1420d5d30a8b8..8f99672c52e801d3ffd3b3045ee5c2cbd4463648 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 8ef7b26e7968e306807f269b864b9a0fb096fee9..24ab31199a3120ce34f9bd4515174ecf233be1e5 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'