diff --git a/build.gradle b/build.gradle index dd593196c4c1636d3f68bec6bdf809ea7979fd89..5dbb31c8e66191bebfac5b0d44c3718d24a7a242 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext.kotlin_version = '1.5.21' - ext.hilt_version = '2.37' + ext.hilt_version = '2.38.1' ext.junit_version = '5.7.2' ext.mockk_version = '1.10.4' repositories { diff --git a/mailbox-android/src/main/AndroidManifest.xml b/mailbox-android/src/main/AndroidManifest.xml index 7ecc940b5595f8575158169b7016c59617df104d..aa9cb5779b2173a559165fbe0a1249064042fdfe 100644 --- a/mailbox-android/src/main/AndroidManifest.xml +++ b/mailbox-android/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:name=".android.MailboxApplication" 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 new file mode 100644 index 0000000000000000000000000000000000000000..b8a6bd90022d3a2f2df6cbc383c9eb3cfe9d6b5f --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt @@ -0,0 +1,14 @@ +package org.briarproject.mailbox.android + +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.CoreModule + +@Module( + includes = [ + CoreModule::class, + ] +) +@InstallIn(SingletonComponent::class) +internal class AppModule diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ApplicationComponent.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ApplicationComponent.kt index 72dab53170f5a22137ce276a388690307bab04b3..c5689ed260bf21589ea5500521cb2000633035f4 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ApplicationComponent.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ApplicationComponent.kt @@ -2,7 +2,11 @@ package org.briarproject.mailbox.android import dagger.Component -@Component +@Component( + modules = [ + AppModule::class, + ] +) interface ApplicationComponent { fun inject(activity: MainActivity) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt index ba58719345388507e89498d934089a8d94aff303..939a5acf167312898f38f00e5f61d7f5d4522eeb 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxApplication.kt @@ -2,10 +2,15 @@ package org.briarproject.mailbox.android import androidx.multidex.MultiDexApplication import dagger.hilt.android.HiltAndroidApp +import org.briarproject.mailbox.core.CoreEagerSingletons +import javax.inject.Inject @HiltAndroidApp class MailboxApplication : MultiDexApplication() { + @Inject + lateinit var coreEagerSingletons: CoreEagerSingletons + override fun onCreate() { super.onCreate() MailboxService.startService(this) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt index b3b91e01d637509f8eb17ff267eca064b286ebed..4461d87f8751fc39013749ab88dcdf8282a379a9 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt @@ -7,7 +7,13 @@ import android.os.IBinder import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import org.briarproject.mailbox.android.MailboxNotificationManager.Companion.NOTIFICATION_MAIN_ID -import org.briarproject.mailbox.core.server.WebServerManager +import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS +import java.util.logging.Level +import java.util.logging.Logger import javax.inject.Inject @AndroidEntryPoint @@ -25,17 +31,43 @@ class MailboxService : Service() { } } + private val LOG = Logger.getLogger(MailboxService::class.java.name) + + @Volatile + internal var started = false + @Inject - internal lateinit var notificationManager: MailboxNotificationManager + internal lateinit var wakeLockManager: AndroidWakeLockManager @Inject - internal lateinit var webServerManager: WebServerManager + internal lateinit var lifecycleManager: LifecycleManager + + @Inject + internal lateinit var notificationManager: MailboxNotificationManager + + override fun onCreate() { + super.onCreate() - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - startForeground(NOTIFICATION_MAIN_ID, notificationManager.serviceNotification) - // TODO handle inside LifecycleManager - webServerManager.start() - return START_NOT_STICKY + // Hold a wake lock during startup + wakeLockManager.runWakefully({ + startForeground(NOTIFICATION_MAIN_ID, notificationManager.serviceNotification) + // Start the services in a background thread + wakeLockManager.executeWakefully({ + val result: StartResult = lifecycleManager.startServices() + if (result === SUCCESS) { + started = true + } else if (result === ALREADY_RUNNING) { + LOG.info("Already running") + stopSelf() + } else { + if (LOG.isLoggable(Level.WARNING)) + LOG.warning("Startup failed: $result") + // TODO: implement this + // showStartupFailure(result) + stopSelf() + } + }, "LifecycleStartup") + }, "LifecycleStartup") } override fun onBind(intent: Intent): IBinder? { @@ -43,8 +75,12 @@ class MailboxService : Service() { } override fun onDestroy() { - // TODO handle inside LifecycleManager - webServerManager.stop() - super.onDestroy() + wakeLockManager.runWakefully({ + super.onDestroy() + wakeLockManager.executeWakefully( + { lifecycleManager.stopServices() }, + "LifecycleShutdown" + ) + }, "LifecycleShutdown") } } diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidExecutor.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..8e9ea64c11cd4d392fae23ef3a04faa259938ffb --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidExecutor.java @@ -0,0 +1,33 @@ +package org.briarproject.mailbox.android.api.system; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Enables background threads to make Android API calls that must be made from + * a thread with a message queue. + */ +public interface AndroidExecutor { + + /** + * Runs the given task on a background thread with a message queue and + * returns a Future for getting the result. + */ + <V> Future<V> runOnBackgroundThread(Callable<V> c); + + /** + * Runs the given task on a background thread with a message queue. + */ + void runOnBackgroundThread(Runnable r); + + /** + * Runs the given task on the main UI thread and returns a Future for + * getting the result. + */ + <V> Future<V> runOnUiThread(Callable<V> c); + + /** + * Runs the given task on the main UI thread. + */ + void runOnUiThread(Runnable r); +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLock.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLock.java new file mode 100644 index 0000000000000000000000000000000000000000..f1577efd080a858ecadfbe4cd8d8416a9c793e2d --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLock.java @@ -0,0 +1,16 @@ +package org.briarproject.mailbox.android.api.system; + +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/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLockManager.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLockManager.java new file mode 100644 index 0000000000000000000000000000000000000000..917d10b191d21421151d2d98b300b7c1408e4bc6 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/api/system/AndroidWakeLockManager.java @@ -0,0 +1,35 @@ +package org.briarproject.mailbox.android.api.system; + +import java.util.concurrent.Executor; + +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/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidExecutorImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidExecutorImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..14a66f08868d961b21aaea616b66bc1a3a2832ad --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidExecutorImpl.java @@ -0,0 +1,75 @@ +package org.briarproject.mailbox.android.system; + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import org.briarproject.mailbox.android.api.system.AndroidExecutor; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.inject.Inject; + +class AndroidExecutorImpl implements AndroidExecutor { + + private final Handler uiHandler; + private final Runnable loop; + private final AtomicBoolean started = new AtomicBoolean(false); + private final CountDownLatch startLatch = new CountDownLatch(1); + + private volatile Handler backgroundHandler = null; + + @Inject + AndroidExecutorImpl(Application app) { + uiHandler = new Handler(app.getApplicationContext().getMainLooper()); + loop = () -> { + Looper.prepare(); + backgroundHandler = new Handler(); + startLatch.countDown(); + Looper.loop(); + }; + } + + private void startIfNecessary() { + if (!started.getAndSet(true)) { + Thread t = new Thread(loop, "AndroidExecutor"); + t.setDaemon(true); + t.start(); + } + try { + startLatch.await(); + } catch (InterruptedException e) { + throw new RejectedExecutionException(e); + } + } + + @Override + public <V> Future<V> runOnBackgroundThread(Callable<V> c) { + FutureTask<V> f = new FutureTask<>(c); + runOnBackgroundThread(f); + return f; + } + + @Override + public void runOnBackgroundThread(Runnable r) { + startIfNecessary(); + backgroundHandler.post(r); + } + + @Override + public <V> Future<V> runOnUiThread(Callable<V> c) { + FutureTask<V> f = new FutureTask<>(c); + runOnUiThread(f); + return f; + } + + @Override + public void runOnUiThread(Runnable r) { + uiHandler.post(r); + } +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidSystemModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidSystemModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..917feb9037102c346ac4dd0df7c4ee1ecd72da7f --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidSystemModule.kt @@ -0,0 +1,60 @@ +package org.briarproject.mailbox.android.system + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.android.api.system.AndroidExecutor +import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import java.util.concurrent.Executor +import java.util.concurrent.RejectedExecutionHandler +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class AndroidSystemModule { + + private var scheduledExecutorService: ScheduledExecutorService + + init { + // Discard tasks that are submitted during shutdown + val policy: RejectedExecutionHandler = ThreadPoolExecutor.DiscardPolicy() + scheduledExecutorService = ScheduledThreadPoolExecutor(1, policy) + } + + @Provides + @Singleton + fun provideScheduledExecutorService( + lifecycleManager: LifecycleManager, + ): ScheduledExecutorService { + lifecycleManager.registerForShutdown(scheduledExecutorService) + return scheduledExecutorService + } + + @Provides + @Singleton + fun provideWakeLockManager( + wakeLockManager: AndroidWakeLockManagerImpl, + ): AndroidWakeLockManager { + return wakeLockManager + } + + @Provides + @Singleton + fun provideAndroidExecutor(androidExecutor: AndroidExecutorImpl): AndroidExecutor { + return androidExecutor + } + + @Provides + @Singleton + fun provideEventExecutor(androidExecutor: AndroidExecutor): Executor { + return Executor { + androidExecutor.runOnUiThread(it) + } + } + +} diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..7d6ac25c0fdd04abd6042995c65d37a2a2be5c4f --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java @@ -0,0 +1,72 @@ +package org.briarproject.mailbox.android.system; + +import static java.util.logging.Level.FINE; +import static java.util.logging.Logger.getLogger; + +import org.briarproject.mailbox.android.api.system.AndroidWakeLock; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +/** + * 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 +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/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockManagerImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..bb590249d6affd5abebd7404e70a63f600e300c2 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockManagerImpl.java @@ -0,0 +1,123 @@ +package org.briarproject.mailbox.android.system; + +import static android.content.Context.POWER_SERVICE; +import static android.os.PowerManager.PARTIAL_WAKE_LOCK; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.PowerManager; + +import org.briarproject.mailbox.android.api.system.AndroidWakeLock; +import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager; + +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; + +import javax.inject.Inject; + +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/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java new file mode 100644 index 0000000000000000000000000000000000000000..d4a910cc144bb899053608210e91f6417995bbf4 --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java @@ -0,0 +1,127 @@ +package org.briarproject.mailbox.android.system; + +import static java.util.Objects.requireNonNull; +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 android.os.PowerManager; +import android.os.PowerManager.WakeLock; + +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; + +@ThreadSafe +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/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/SharedWakeLock.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/SharedWakeLock.java new file mode 100644 index 0000000000000000000000000000000000000000..a4e353de45c2e690a75299f698b3371968ff2f9f --- /dev/null +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/SharedWakeLock.java @@ -0,0 +1,21 @@ +package org.briarproject.mailbox.android.system; + +import org.briarproject.mailbox.android.api.system.AndroidWakeLock; + +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(); + +} diff --git a/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd82487df2d06121f85f7994728f1070..ac94b34f54e396eb7aa0f59dc834ac07f4741db5 100644 --- a/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@drawable/ic_launcher_foreground"/> + <background android:drawable="@color/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> </adaptive-icon> \ No newline at end of file diff --git a/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd82487df2d06121f85f7994728f1070..ac94b34f54e396eb7aa0f59dc834ac07f4741db5 100644 --- a/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/mailbox-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@drawable/ic_launcher_foreground"/> + <background android:drawable="@color/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> </adaptive-icon> \ No newline at end of file diff --git a/mailbox-android/src/main/res/values-night/themes.xml b/mailbox-android/src/main/res/values-night/themes.xml index f40cdc54bcc254a36b6ca9608df6e9b741cd8ec9..8b6f2d272f57a825e5e3f2bbf67c1443a2df7b40 100644 --- a/mailbox-android/src/main/res/values-night/themes.xml +++ b/mailbox-android/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ -<resources xmlns:tools="http://schemas.android.com/tools"> +<resources> <!-- Base application theme. --> <style name="Theme.Briarmailbox" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Primary brand color. --> diff --git a/mailbox-android/src/main/res/values/themes.xml b/mailbox-android/src/main/res/values/themes.xml index d31cb1eff1af1a7a05d903c00d9c546f44a62f69..9d0dfedbc6008d4403b648f4fb6c3393cba66d18 100644 --- a/mailbox-android/src/main/res/values/themes.xml +++ b/mailbox-android/src/main/res/values/themes.xml @@ -1,4 +1,4 @@ -<resources xmlns:tools="http://schemas.android.com/tools"> +<resources> <!-- Base application theme. --> <style name="Theme.Briarmailbox" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Primary brand color. --> diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt index ab13693777396c584feda930da7f410bb4cda41e..d8eec8e63a3d539b4824ec46ada496e94acd274f 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt @@ -2,20 +2,13 @@ package org.briarproject.mailbox.cli import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.counted -import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.int import org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY -import java.io.File -import java.io.File.separator -import java.io.IOException -import java.lang.System.getProperty import java.lang.System.setProperty -import java.nio.file.Files.setPosixFilePermissions -import java.nio.file.attribute.PosixFilePermission -import java.nio.file.attribute.PosixFilePermission.* -import java.util.logging.Level.* +import java.util.logging.Level.ALL +import java.util.logging.Level.INFO +import java.util.logging.Level.WARNING import java.util.logging.LogManager private class Main : CliktCommand( diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle index b61ec076352d3231c5794d2b3cbbba2c1ec15d46..82bc45a5ca36a534ccd29e24542f97f306d71895 100644 --- a/mailbox-core/build.gradle +++ b/mailbox-core/build.gradle @@ -10,8 +10,10 @@ targetCompatibility = 1.8 dependencies { api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + api 'com.google.code.findbugs:jsr305:3.0.2' + api 'javax.inject:javax.inject:1' // required for @Qualifier in @Wakeful - implementation "com.google.dagger:dagger:$hilt_version" + implementation "com.google.dagger:hilt-core:$hilt_version" kapt "com.google.dagger:dagger-compiler:$hilt_version" def ktorVersion = '1.6.2' diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1e7c297842170d682d509bd1d5b764f5bca4669 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreEagerSingletonsModule.kt @@ -0,0 +1,28 @@ +package org.briarproject.mailbox.core + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.server.WebServerManager +import javax.inject.Inject +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class CoreEagerSingletonsModule { + + @Provides + @Singleton + fun provideEagerSingletons(webServerManager: WebServerManager): CoreEagerSingletons { + return CoreEagerSingletons() + } + +} + +class CoreEagerSingletons { + + @Inject + internal lateinit var webServerManager: WebServerManager + +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b14ecb17627129c73639980eb90ffce490c6d0c6 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt @@ -0,0 +1,19 @@ +package org.briarproject.mailbox.core + +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.db.DatabaseModule +import org.briarproject.mailbox.core.lifecycle.LifecycleModule +import org.briarproject.mailbox.core.server.WebServerModule + +@Module( + includes = [ + CoreEagerSingletonsModule::class, + LifecycleModule::class, + DatabaseModule::class, + WebServerModule::class, + ] +) +@InstallIn(SingletonComponent::class) +class CoreModule \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/Service.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/Service.kt deleted file mode 100644 index 7edf91883510e5ee7f410e877a7fa658fcd0f9df..0000000000000000000000000000000000000000 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/Service.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.briarproject.mailbox.core - -interface Service { -} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..88e0e248e8413ebe18e2e1728d623f167e35ce26 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java @@ -0,0 +1,21 @@ +package org.briarproject.mailbox.core.db; + +import javax.annotation.Nullable; + +/** + * Encapsulates the database implementation and exposes high-level operations + * to other components. + */ +public interface DatabaseComponent { + + /** + * Opens the database and returns true if the database already existed. + */ + boolean open(@Nullable MigrationListener listener); + + /** + * Waits for any open transactions to finish and closes the database. + */ + void close(); + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..c82dd66dc8bf6be7c3514c3bea98dbf56189c0fe --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt @@ -0,0 +1,14 @@ +package org.briarproject.mailbox.core.db + +class DatabaseComponentImpl : DatabaseComponent { + + override fun open(listener: MigrationListener?): Boolean { + // TODO: implement this + return true; + } + + override fun close() { + // TODO: implement this + } + +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..851fcbe413325f6beda34db552e1f183ae9c830a --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt @@ -0,0 +1,19 @@ +package org.briarproject.mailbox.core.db + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class DatabaseModule { + + @Provides + @Singleton + fun provideDatabaseComponent(): DatabaseComponent { + return DatabaseComponentImpl() + } + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e12dece37e94b43206a469b8a5521fd73e475d9b --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java @@ -0,0 +1,15 @@ +package org.briarproject.mailbox.core.db; + +public interface MigrationListener { + + /** + * This is called when a migration is started while opening the database. + * It will be called once for each migration being applied. + */ + void onDatabaseMigration(); + + /** + * This is called when compaction is started while opening the database. + */ + void onDatabaseCompaction(); +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java new file mode 100644 index 0000000000000000000000000000000000000000..8bf4e72483e842b7b12b10cf7f433ae6c07bd9f4 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java @@ -0,0 +1,103 @@ +package org.briarproject.mailbox.core.lifecycle; + +import org.briarproject.mailbox.core.db.DatabaseComponent; +import org.briarproject.mailbox.core.system.Wakeful; + +import java.util.concurrent.ExecutorService; + +/** + * Manages the lifecycle of the app: opening and closing the + * {@link DatabaseComponent} starting and stopping {@link Service Services}, + * and shutting down {@link ExecutorService ExecutorServices}. + */ +public interface LifecycleManager { + + /** + * The result of calling {@link #startServices()}. + */ + enum StartResult { + ALREADY_RUNNING, + SERVICE_ERROR, + SUCCESS + } + + /** + * The state the lifecycle can be in. + * Returned by {@link #getLifecycleState()} + */ + enum LifecycleState { + + STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES, + RUNNING, STOPPING; + + public boolean isAfter(LifecycleState state) { + return ordinal() > state.ordinal(); + } + } + + /** + * Registers a hook to be called after the database is opened and before + * {@link Service services} are started. This method should be called + * before {@link #startServices()}. + */ + void registerOpenDatabaseHook(OpenDatabaseHook hook); + + /** + * Registers a {@link Service} to be started and stopped. This method + * should be called before {@link #startServices()}. + */ + void registerService(Service s); + + /** + * Registers an {@link ExecutorService} to be shut down. This method + * should be called before {@link #startServices()}. + */ + void registerForShutdown(ExecutorService e); + + /** + * Opens the {@link DatabaseComponent} using the given key and starts any + * registered {@link Service Services}. + */ + @Wakeful + StartResult startServices(); + + /** + * Stops any registered {@link Service Services}, shuts down any + * registered {@link ExecutorService ExecutorServices}, and closes the + * {@link DatabaseComponent}. + */ + @Wakeful + void stopServices(); + + /** + * Waits for the {@link DatabaseComponent} to be opened before returning. + */ + void waitForDatabase() throws InterruptedException; + + /** + * Waits for the {@link DatabaseComponent} to be opened and all registered + * {@link Service Services} to start before returning. + */ + void waitForStartup() throws InterruptedException; + + /** + * Waits for all registered {@link Service Services} to stop, all + * registered {@link ExecutorService ExecutorServices} to shut down, and + * the {@link DatabaseComponent} to be closed before returning. + */ + void waitForShutdown() throws InterruptedException; + + /** + * Returns the current state of the lifecycle. + */ + LifecycleState getLifecycleState(); + + interface OpenDatabaseHook { + /** + * Called when the database is being opened, before + * {@link #waitForDatabase()} returns. + */ + @Wakeful + void onDatabaseOpened(); + } +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..982a62ca34ca354a90e52a6ad8bddc6a15e60726 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java @@ -0,0 +1,183 @@ +package org.briarproject.mailbox.core.lifecycle; + +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.RUNNING; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPING; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR; +import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS; +import static org.briarproject.mailbox.core.util.LogUtils.logDuration; +import static org.briarproject.mailbox.core.util.LogUtils.logException; +import static org.briarproject.mailbox.core.util.LogUtils.now; +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 org.briarproject.mailbox.core.db.DatabaseComponent; +import org.briarproject.mailbox.core.db.MigrationListener; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Semaphore; +import java.util.logging.Logger; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +@ThreadSafe +class LifecycleManagerImpl implements LifecycleManager, MigrationListener { + + private static final Logger LOG = + getLogger(LifecycleManagerImpl.class.getName()); + + private final DatabaseComponent db; + private final List<Service> services; + private final List<OpenDatabaseHook> openDatabaseHooks; + private final List<ExecutorService> executors; + private final Semaphore startStopSemaphore = new Semaphore(1); + private final CountDownLatch dbLatch = new CountDownLatch(1); + private final CountDownLatch startupLatch = new CountDownLatch(1); + private final CountDownLatch shutdownLatch = new CountDownLatch(1); + + private volatile LifecycleState state = STARTING; + + @Inject + LifecycleManagerImpl(DatabaseComponent db) { + this.db = db; + services = new CopyOnWriteArrayList<>(); + openDatabaseHooks = new CopyOnWriteArrayList<>(); + executors = new CopyOnWriteArrayList<>(); + } + + @Override + public void registerService(Service s) { + if (LOG.isLoggable(INFO)) + LOG.info("Registering service " + s.getClass().getSimpleName()); + services.add(s); + } + + @Override + public void registerOpenDatabaseHook(OpenDatabaseHook hook) { + if (LOG.isLoggable(INFO)) { + LOG.info("Registering open database hook " + + hook.getClass().getSimpleName()); + } + openDatabaseHooks.add(hook); + } + + @Override + public void registerForShutdown(ExecutorService e) { + LOG.info("Registering executor " + e.getClass().getSimpleName()); + executors.add(e); + } + + @Override + public StartResult startServices() { + if (!startStopSemaphore.tryAcquire()) { + LOG.info("Already starting or stopping"); + return ALREADY_RUNNING; + } + try { + LOG.info("Opening database"); + long start = now(); + boolean reopened = db.open(this); + if (reopened) logDuration(LOG, "Reopening database", start); + else logDuration(LOG, "Creating database", start); + + LOG.info("Starting services"); + state = STARTING_SERVICES; + dbLatch.countDown(); + + for (Service s : services) { + start = now(); + s.startService(); + if (LOG.isLoggable(FINE)) { + logDuration(LOG, "Starting service " + + s.getClass().getSimpleName(), start); + } + } + + state = RUNNING; + startupLatch.countDown(); + return SUCCESS; + } catch (ServiceException e) { + logException(LOG, WARNING, e); + return SERVICE_ERROR; + } finally { + startStopSemaphore.release(); + } + } + + @Override + public void onDatabaseMigration() { + state = MIGRATING_DATABASE; + } + + @Override + public void onDatabaseCompaction() { + state = COMPACTING_DATABASE; + } + + @Override + public void stopServices() { + try { + startStopSemaphore.acquire(); + } catch (InterruptedException e) { + LOG.warning("Interrupted while waiting to stop services"); + return; + } + try { + LOG.info("Stopping services"); + state = STOPPING; + for (Service s : services) { + long start = now(); + s.stopService(); + if (LOG.isLoggable(FINE)) { + logDuration(LOG, "Stopping service " + + s.getClass().getSimpleName(), start); + } + } + for (ExecutorService e : executors) { + if (LOG.isLoggable(FINE)) { + LOG.fine("Stopping executor " + + e.getClass().getSimpleName()); + } + e.shutdownNow(); + } + long start = now(); + db.close(); + logDuration(LOG, "Closing database", start); + shutdownLatch.countDown(); + } catch (ServiceException e) { + logException(LOG, WARNING, e); + } finally { + startStopSemaphore.release(); + } + } + + @Override + public void waitForDatabase() throws InterruptedException { + dbLatch.await(); + } + + @Override + public void waitForStartup() throws InterruptedException { + startupLatch.await(); + } + + @Override + public void waitForShutdown() throws InterruptedException { + shutdownLatch.await(); + } + + @Override + public LifecycleState getLifecycleState() { + return state; + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..781ef6a21f0e630c95f935d2cdd18a25f4283b73 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleModule.kt @@ -0,0 +1,19 @@ +package org.briarproject.mailbox.core.lifecycle + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class LifecycleModule { + + @Provides + @Singleton + fun provideLifecycleManager(lifecycleManager: LifecycleManagerImpl): LifecycleManager { + return lifecycleManager + } + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/Service.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/Service.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd1a9a86e91cc2c14d5a29182ed9445dc7c3be8b --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/Service.kt @@ -0,0 +1,22 @@ +package org.briarproject.mailbox.core.lifecycle + +import org.briarproject.mailbox.core.system.Wakeful + +interface Service { + + /** + * Starts the service. This method must not be called concurrently with [stopService]. + */ + @Wakeful + @Throws(ServiceException::class) + fun startService() + + /** + * Stops the service. This method must not be called concurrently with + * [startService]. + */ + @Wakeful + @Throws(ServiceException::class) + fun stopService() + +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java new file mode 100644 index 0000000000000000000000000000000000000000..adaec28c8a6c88467f98330793ac9e935f6b9b83 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java @@ -0,0 +1,15 @@ +package org.briarproject.mailbox.core.lifecycle; + +/** + * An exception that indicates an error starting or stopping a {@link Service}. + */ +public class ServiceException extends Exception { + + public ServiceException() { + super(); + } + + public ServiceException(Throwable cause) { + super(cause); + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt index 3466ed1ca2db1dd9e0c0d47a6c13482ea01e1629..8741eb1edb031ffd63cfcc69a9cca35520d8ccb0 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt @@ -7,12 +7,13 @@ import io.ktor.server.netty.Netty import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import org.briarproject.mailbox.core.lifecycle.Service import java.util.logging.Logger.getLogger import javax.inject.Inject import javax.inject.Singleton @Singleton -class WebServerManager @Inject constructor() { +class WebServerManager @Inject constructor() : Service { internal companion object { private const val PORT = 8888 @@ -26,7 +27,7 @@ class WebServerManager @Inject constructor() { } } - fun start() { + override fun startService() { // hangs if not starting inside a coroutine GlobalScope.launch(Dispatchers.IO) { LOG.info("starting") @@ -35,7 +36,7 @@ class WebServerManager @Inject constructor() { } } - fun stop() { + override fun stopService() { server.stop(1_000, 2_000) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..37550a3823539a0ab420a0b060c9c0f6a9a6b0aa --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt @@ -0,0 +1,22 @@ +package org.briarproject.mailbox.core.server + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class WebServerModule { + + @Provides + @Singleton + fun provideWebServer(lifecycleManager: LifecycleManager): WebServerManager { + val webServerManager = WebServerManager() + lifecycleManager.registerService(webServerManager) + return webServerManager + } + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Wakeful.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Wakeful.java new file mode 100644 index 0000000000000000000000000000000000000000..a34038db1ad182d519a92b2d94ff46d9328512ea --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Wakeful.java @@ -0,0 +1,19 @@ +package org.briarproject.mailbox.core.system; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Annotation for methods that must be called while holding a wake lock, if + * the platform supports wake locks. + */ +@Qualifier +@Target(METHOD) +@Retention(RUNTIME) +public @interface Wakeful { +} \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3fb3434b8337c028aef87b6b7bc8c69b4ea02e82 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java @@ -0,0 +1,60 @@ +package org.briarproject.mailbox.core.util; + +import static java.util.logging.Level.FINE; + +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LogUtils { + + private static final int NANOS_PER_MILLI = 1000 * 1000; + + /** + * Returns the elapsed time in milliseconds since some arbitrary + * starting time. This is only useful for measuring elapsed time. + */ + public static long now() { + return System.nanoTime() / NANOS_PER_MILLI; + } + + /** + * Logs the duration of a task. + * + * @param logger the logger to use + * @param task a description of the task + * @param start the start time of the task, as returned by {@link #now()} + */ + public static void logDuration(Logger logger, String task, long start) { + if (logger.isLoggable(FINE)) { + long duration = now() - start; + logger.fine(task + " took " + duration + " ms"); + } + } + + public static void logException(Logger logger, Level level, Throwable t) { + if (logger.isLoggable(level)) logger.log(level, t.toString(), t); + } + + public static void logFileOrDir(Logger logger, Level level, File f) { + if (logger.isLoggable(level)) { + if (f.isFile()) { + logWithType(logger, level, f, "F"); + } else if (f.isDirectory()) { + logWithType(logger, level, f, "D"); + File[] children = f.listFiles(); + if (children != null) { + for (File child : children) + logFileOrDir(logger, level, child); + } + } else if (f.exists()) { + logWithType(logger, level, f, "?"); + } + } + } + + private static void logWithType(Logger logger, Level level, File f, + String type) { + logger.log(level, type + " " + f.getAbsolutePath() + " " + f.length()); + } +}