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