diff --git a/build.gradle b/build.gradle
index 785ae44a042222efd8a6119920f3976c2e00ed75..3595ebc6856195e63fab0fdd1f306896dbddc2b5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,12 @@
 buildscript {
-    ext.kotlin_version = '1.6.20'
+    ext.kotlin_version = '1.8.0'
     repositories {
         mavenCentral()
         google()
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.0.4'
+        classpath 'com.android.tools.build:gradle:7.4.2'
         classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
diff --git a/gradle.properties b/gradle.properties
index 8478b5b92bbb8ab59e193123ada9c159134a5cd7..d202c7761af681a66f9558993abed3b1bfb67d3d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 GROUP=org.briarproject
 POM_ARTIFACT_ID=dont-kill-me-lib
-VERSION_NAME=0.2.5
+VERSION_NAME=0.2.6
 
 POM_NAME=Do not kill me library
 POM_DESCRIPTION=An Android library helping to keep a foreground service with wake-locks running. No other use-cases considered.
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0a0271406935e2092b9fe6b805c0a5b588d4e7a3..9ff0819bc5252df7fcdd1b2eee444071214bd7cf 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
-distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d
\ No newline at end of file
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+distributionSha256Sum=cb87f222c5585bd46838ad4db78463a5c5f3d336e5e2b98dc7c0c586527351c2
\ No newline at end of file
diff --git a/lib/build.gradle b/lib/build.gradle
index e1e55a9c10c0027ddbd6e407b1f9e57c5aa89b8e..b3cff4f36b48b19d5e5ea3aaa7fb8c062de0fa2a 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -6,11 +6,11 @@ plugins {
 }
 
 android {
-    compileSdkVersion 31
+    compileSdkVersion 33
 
     defaultConfig {
         minSdk 16
-        targetSdk 30
+        targetSdk 33
 
         consumerProguardFiles "consumer-rules.pro"
     }
@@ -39,6 +39,10 @@ kotlin {
 
 dependencies {
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'org.briarproject:null-safety:0.1'
+    implementation 'com.google.code.findbugs:jsr305:3.0.2'
+    implementation 'javax.inject:javax.inject:1'
+
     testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
 }
 
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLock.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..fbdf9452bacfb6ecab28644b1364abe3b3123379
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLock.java
@@ -0,0 +1,22 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+public interface AndroidWakeLock {
+
+	/**
+	 * Acquires the wake lock. This has no effect if the wake lock has already
+	 * been acquired.
+	 */
+	void acquire();
+
+	/**
+	 * Releases the wake lock. This has no effect if the wake lock has already
+	 * been released.
+	 */
+	void release();
+}
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockImpl.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..af994c7119df2f8ad5d6f3d3ebf391a26470cfdd
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockImpl.java
@@ -0,0 +1,73 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import static java.util.logging.Level.FINE;
+import static java.util.logging.Logger.getLogger;
+
+/**
+ * A wrapper around a {@link SharedWakeLock} that provides the more convenient
+ * semantics of {@link AndroidWakeLock} (i.e. calls to acquire() and release()
+ * don't need to be balanced).
+ */
+@ThreadSafe
+@NotNullByDefault
+class AndroidWakeLockImpl implements AndroidWakeLock {
+
+	private static final Logger LOG =
+			getLogger(AndroidWakeLockImpl.class.getName());
+
+	private static final AtomicInteger INSTANCE_ID = new AtomicInteger(0);
+
+	private final SharedWakeLock sharedWakeLock;
+	private final String tag;
+
+	private final Object lock = new Object();
+	@GuardedBy("lock")
+	private boolean held = false;
+
+	AndroidWakeLockImpl(SharedWakeLock sharedWakeLock, String tag) {
+		this.sharedWakeLock = sharedWakeLock;
+		this.tag = tag + "_" + INSTANCE_ID.getAndIncrement();
+	}
+
+	@Override
+	public void acquire() {
+		synchronized (lock) {
+			if (held) {
+				if (LOG.isLoggable(FINE)) {
+					LOG.fine(tag + " already acquired");
+				}
+			} else {
+				if (LOG.isLoggable(FINE)) {
+					LOG.fine(tag + " acquiring shared wake lock");
+				}
+				held = true;
+				sharedWakeLock.acquire();
+			}
+		}
+	}
+
+	@Override
+	public void release() {
+		synchronized (lock) {
+			if (held) {
+				if (LOG.isLoggable(FINE)) {
+					LOG.fine(tag + " releasing shared wake lock");
+				}
+				held = false;
+				sharedWakeLock.release();
+			} else {
+				if (LOG.isLoggable(FINE)) {
+					LOG.fine(tag + " already released");
+				}
+			}
+		}
+	}
+}
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManager.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..89b2245e81b924875a72de1cb8fde99992c62879
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManager.java
@@ -0,0 +1,38 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.util.concurrent.Executor;
+
+@NotNullByDefault
+public interface AndroidWakeLockManager {
+
+	/**
+	 * Creates a wake lock with the given tag. The tag is only used for
+	 * logging; the underlying OS wake lock will use its own tag.
+	 */
+	AndroidWakeLock createWakeLock(String tag);
+
+	/**
+	 * Runs the given task while holding a wake lock.
+	 */
+	void runWakefully(Runnable r, String tag);
+
+	/**
+	 * Submits the given task to the given executor while holding a wake lock.
+	 * The lock is released when the task completes, or if an exception is
+	 * thrown while submitting or running the task.
+	 */
+	void executeWakefully(Runnable r, Executor executor, String tag);
+
+	/**
+	 * Starts a dedicated thread to run the given task asynchronously. A wake
+	 * lock is acquired before starting the thread and released when the task
+	 * completes, or if an exception is thrown while starting the thread or
+	 * running the task.
+	 * <p>
+	 * This method should only be used for lifecycle management tasks that
+	 * can't be run on an executor.
+	 */
+	void executeWakefully(Runnable r, String tag);
+}
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManagerImpl.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..d11df86056ba913e9db07b93ce7dbd4eba8d92ea
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/AndroidWakeLockManagerImpl.java
@@ -0,0 +1,123 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.PowerManager;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+import javax.inject.Inject;
+
+import static android.content.Context.POWER_SERVICE;
+import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.nullsafety.NullSafety.requireNonNull;
+
+@NotNullByDefault
+class AndroidWakeLockManagerImpl implements AndroidWakeLockManager {
+
+	/**
+	 * How often to replace the wake lock.
+	 */
+	private static final long LOCK_DURATION_MS = MINUTES.toMillis(1);
+
+	/**
+	 * Automatically release the lock this many milliseconds after it's due
+	 * to have been replaced and released.
+	 */
+	private static final long SAFETY_MARGIN_MS = SECONDS.toMillis(30);
+
+	private final SharedWakeLock sharedWakeLock;
+
+	@Inject
+	AndroidWakeLockManagerImpl(Application app,
+			ScheduledExecutorService scheduledExecutorService) {
+		PowerManager powerManager = (PowerManager)
+				requireNonNull(app.getSystemService(POWER_SERVICE));
+		String tag = getWakeLockTag(app);
+		sharedWakeLock = new RenewableWakeLock(powerManager,
+				scheduledExecutorService, PARTIAL_WAKE_LOCK, tag,
+				LOCK_DURATION_MS, SAFETY_MARGIN_MS);
+	}
+
+	@Override
+	public AndroidWakeLock createWakeLock(String tag) {
+		return new AndroidWakeLockImpl(sharedWakeLock, tag);
+	}
+
+	@Override
+	public void runWakefully(Runnable r, String tag) {
+		AndroidWakeLock wakeLock = createWakeLock(tag);
+		wakeLock.acquire();
+		try {
+			r.run();
+		} finally {
+			wakeLock.release();
+		}
+	}
+
+	@Override
+	public void executeWakefully(Runnable r, Executor executor, String tag) {
+		AndroidWakeLock wakeLock = createWakeLock(tag);
+		wakeLock.acquire();
+		try {
+			executor.execute(() -> {
+				try {
+					r.run();
+				} finally {
+					// Release the wake lock if the task throws an exception
+					wakeLock.release();
+				}
+			});
+		} catch (Exception e) {
+			// Release the wake lock if the executor throws an exception when
+			// we submit the task (in which case the release() call above won't
+			// happen)
+			wakeLock.release();
+			throw e;
+		}
+	}
+
+	@Override
+	public void executeWakefully(Runnable r, String tag) {
+		AndroidWakeLock wakeLock = createWakeLock(tag);
+		wakeLock.acquire();
+		try {
+			new Thread(() -> {
+				try {
+					r.run();
+				} finally {
+					wakeLock.release();
+				}
+			}).start();
+		} catch (Exception e) {
+			wakeLock.release();
+			throw e;
+		}
+	}
+
+	private String getWakeLockTag(Context ctx) {
+		PackageManager pm = ctx.getPackageManager();
+		if (isInstalled(pm, "com.huawei.powergenie")) {
+			return "LocationManagerService";
+		} else if (isInstalled(pm, "com.evenwell.PowerMonitor")) {
+			return "AudioIn";
+		}
+		return ctx.getPackageName();
+	}
+
+	private boolean isInstalled(PackageManager pm, String packageName) {
+		try {
+			pm.getPackageInfo(packageName, 0);
+			return true;
+		} catch (PackageManager.NameNotFoundException e) {
+			return false;
+		}
+	}
+
+}
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/RenewableWakeLock.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/RenewableWakeLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ed18ff68f35cdc401487d17be1512b31ab06990
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/RenewableWakeLock.java
@@ -0,0 +1,130 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Level.FINE;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.nullsafety.NullSafety.requireNonNull;
+
+@ThreadSafe
+@NotNullByDefault
+class RenewableWakeLock implements SharedWakeLock {
+
+	private static final Logger LOG =
+			getLogger(RenewableWakeLock.class.getName());
+
+	private final PowerManager powerManager;
+	private final ScheduledExecutorService scheduledExecutorService;
+	private final int levelAndFlags;
+	private final String tag;
+	private final long durationMs, safetyMarginMs;
+
+	private final Object lock = new Object();
+	@GuardedBy("lock")
+	@Nullable
+	private WakeLock wakeLock;
+	@GuardedBy("lock")
+	@Nullable
+	private Future<?> future;
+	@GuardedBy("lock")
+	private int refCount = 0;
+	@GuardedBy("lock")
+	private long acquired = 0;
+
+	RenewableWakeLock(PowerManager powerManager,
+			ScheduledExecutorService scheduledExecutorService,
+			int levelAndFlags,
+			String tag,
+			long durationMs,
+			long safetyMarginMs) {
+		this.powerManager = powerManager;
+		this.scheduledExecutorService = scheduledExecutorService;
+		this.levelAndFlags = levelAndFlags;
+		this.tag = tag;
+		this.durationMs = durationMs;
+		this.safetyMarginMs = safetyMarginMs;
+	}
+
+	@Override
+	public void acquire() {
+		synchronized (lock) {
+			refCount++;
+			if (refCount == 1) {
+				if (LOG.isLoggable(INFO)) {
+					LOG.info("Acquiring wake lock " + tag);
+				}
+				wakeLock = powerManager.newWakeLock(levelAndFlags, tag);
+				// We do our own reference counting so we can replace the lock
+				// TODO: Check whether using a ref-counted wake lock affects
+				//  power management apps
+				wakeLock.setReferenceCounted(false);
+				wakeLock.acquire(durationMs + safetyMarginMs);
+				future = scheduledExecutorService.schedule(this::renew,
+						durationMs, MILLISECONDS);
+				acquired = android.os.SystemClock.elapsedRealtime();
+			} else if (LOG.isLoggable(FINE)) {
+				LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
+			}
+		}
+	}
+
+	private void renew() {
+		if (LOG.isLoggable(INFO)) LOG.info("Renewing wake lock " + tag);
+		synchronized (lock) {
+			if (wakeLock == null) {
+				LOG.info("Already released");
+				return;
+			}
+			if (LOG.isLoggable(FINE)) {
+				LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
+			}
+			long now = android.os.SystemClock.elapsedRealtime();
+			long expiry = acquired + durationMs + safetyMarginMs;
+			if (now > expiry && LOG.isLoggable(WARNING)) {
+				LOG.warning("Wake lock expired " + (now - expiry) + " ms ago");
+			}
+			WakeLock oldWakeLock = wakeLock;
+			wakeLock = powerManager.newWakeLock(levelAndFlags, tag);
+			wakeLock.setReferenceCounted(false);
+			wakeLock.acquire(durationMs + safetyMarginMs);
+			oldWakeLock.release();
+			future = scheduledExecutorService.schedule(this::renew, durationMs,
+					MILLISECONDS);
+			acquired = now;
+		}
+	}
+
+	@Override
+	public void release() {
+		synchronized (lock) {
+			refCount--;
+			if (refCount == 0) {
+				if (LOG.isLoggable(INFO)) {
+					LOG.info("Releasing wake lock " + tag);
+				}
+				requireNonNull(future).cancel(false);
+				future = null;
+				requireNonNull(wakeLock).release();
+				wakeLock = null;
+				acquired = 0;
+			} else if (LOG.isLoggable(FINE)) {
+				LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
+			}
+		}
+	}
+}
+
diff --git a/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/SharedWakeLock.java b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/SharedWakeLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..73cffe7200fd46db4e486afb0f6f0d43d20ef13a
--- /dev/null
+++ b/lib/src/main/java/org/briarproject/android/dontkillmelib/wakelock/SharedWakeLock.java
@@ -0,0 +1,21 @@
+package org.briarproject.android.dontkillmelib.wakelock;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface SharedWakeLock {
+
+	/**
+	 * Acquires the wake lock. This increments the wake lock's reference count,
+	 * so unlike {@link AndroidWakeLock#acquire()} every call to this method
+	 * must be followed by a balancing call to {@link #release()}.
+	 */
+	void acquire();
+
+	/**
+	 * Releases the wake lock. This decrements the wake lock's reference count,
+	 * so unlike {@link AndroidWakeLock#release()} every call to this method
+	 * must follow a balancing call to {@link #acquire()}.
+	 */
+	void release();
+}