diff --git a/k9mail/src/main/java/com/fsck/k9/K9.java b/k9mail/src/main/java/com/fsck/k9/K9.java index 36eaedf6d..dfdd96519 100644 --- a/k9mail/src/main/java/com/fsck/k9/K9.java +++ b/k9mail/src/main/java/com/fsck/k9/K9.java @@ -38,6 +38,7 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.ssl.LocalKeyStore; import com.fsck.k9.mailstore.LocalStore; +import com.fsck.k9.power.DeviceIdleManager; import com.fsck.k9.preferences.Storage; import com.fsck.k9.preferences.StorageEditor; import com.fsck.k9.provider.UnreadWidgetProvider; @@ -353,10 +354,22 @@ public class K9 extends Application { * whether any accounts are configured. */ public static void setServicesEnabled(Context context) { - int acctLength = Preferences.getPreferences(context).getAvailableAccounts().size(); + Context appContext = context.getApplicationContext(); + int acctLength = Preferences.getPreferences(appContext).getAvailableAccounts().size(); + boolean enable = acctLength > 0; - setServicesEnabled(context, acctLength > 0, null); + setServicesEnabled(appContext, enable, null); + updateDeviceIdleReceiver(appContext, enable); + } + + private static void updateDeviceIdleReceiver(Context context, boolean enable) { + DeviceIdleManager deviceIdleManager = DeviceIdleManager.getInstance(context); + if (enable) { + deviceIdleManager.registerReceiver(); + } else { + deviceIdleManager.unregisterReceiver(); + } } private static void setServicesEnabled(Context context, boolean enabled, Integer wakeLockId) { diff --git a/k9mail/src/main/java/com/fsck/k9/helper/K9AlarmManager.java b/k9mail/src/main/java/com/fsck/k9/helper/K9AlarmManager.java index 24dbf67ad..5921477fa 100644 --- a/k9mail/src/main/java/com/fsck/k9/helper/K9AlarmManager.java +++ b/k9mail/src/main/java/com/fsck/k9/helper/K9AlarmManager.java @@ -1,56 +1,48 @@ package com.fsck.k9.helper; -import android.annotation.TargetApi; + import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.os.Build; -import android.os.PowerManager; +import android.support.annotation.RequiresApi; import android.support.annotation.VisibleForTesting; +import com.fsck.k9.power.DozeChecker; + public class K9AlarmManager { private final AlarmManager alarmManager; - private final PowerManager powerManager; - private final String packageName; + private final DozeChecker dozeChecker; - @VisibleForTesting - K9AlarmManager(Context context) { - alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - packageName = context.getPackageName(); - } - public static K9AlarmManager getAlarmManager(Context context) { - return new K9AlarmManager(context); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + DozeChecker dozeChecker = new DozeChecker(context); + return new K9AlarmManager(alarmManager, dozeChecker); + } + + @VisibleForTesting + K9AlarmManager(AlarmManager alarmManager, DozeChecker dozeChecker) { + this.alarmManager = alarmManager; + this.dozeChecker = dozeChecker; } public void set(int type, long triggerAtMillis, PendingIntent operation) { - if (isDozeSupported() && isDozeWhiteListed()) { + if (dozeChecker.isDeviceIdleModeSupported() && dozeChecker.isAppWhitelisted()) { setAndAllowWhileIdle(type, triggerAtMillis, operation); } else { alarmManager.set(type, triggerAtMillis, operation); } } - @TargetApi(Build.VERSION_CODES.M) - public void setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) { + @RequiresApi(Build.VERSION_CODES.M) + private void setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) { alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation); } public void cancel(PendingIntent operation) { alarmManager.cancel(operation); } - - @VisibleForTesting - protected boolean isDozeSupported() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - } - - @TargetApi(Build.VERSION_CODES.M) - private boolean isDozeWhiteListed() { - return powerManager.isIgnoringBatteryOptimizations(packageName); - } } diff --git a/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleManager.java b/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleManager.java new file mode 100644 index 000000000..c8d364641 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleManager.java @@ -0,0 +1,84 @@ +package com.fsck.k9.power; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.IntentFilter; +import android.os.Build; +import android.os.PowerManager; + +import timber.log.Timber; + + +public abstract class DeviceIdleManager { + private static DeviceIdleManager instance; + + + public static synchronized DeviceIdleManager getInstance(Context context) { + if (instance == null) { + DozeChecker dozeChecker = new DozeChecker(context); + if (dozeChecker.isDeviceIdleModeSupported() && !dozeChecker.isAppWhitelisted()) { + instance = RealDeviceIdleManager.newInstance(context); + } else { + instance = new NoOpDeviceIdleManager(); + } + } + return instance; + } + + private DeviceIdleManager() { + } + + public abstract void registerReceiver(); + public abstract void unregisterReceiver(); + + + static class NoOpDeviceIdleManager extends DeviceIdleManager { + @Override + public void registerReceiver() { + // Do nothing + } + + @Override + public void unregisterReceiver() { + // Do nothing + } + } + + @TargetApi(Build.VERSION_CODES.M) + static class RealDeviceIdleManager extends DeviceIdleManager { + private final Context context; + private final DeviceIdleReceiver deviceIdleReceiver; + private final IntentFilter intentFilter; + private boolean registered; + + + static RealDeviceIdleManager newInstance(Context context) { + Context appContext = context.getApplicationContext(); + return new RealDeviceIdleManager(appContext); + } + + private RealDeviceIdleManager(Context context) { + this.context = context; + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + deviceIdleReceiver = new DeviceIdleReceiver(powerManager); + intentFilter = new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } + + @Override + public void registerReceiver() { + Timber.v("Registering DeviceIdleReceiver"); + registered = true; + context.registerReceiver(deviceIdleReceiver, intentFilter); + } + + @Override + public void unregisterReceiver() { + Timber.v("Unregistering DeviceIdleReceiver"); + if (registered) { + context.unregisterReceiver(deviceIdleReceiver); + registered = false; + } + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleReceiver.java b/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleReceiver.java new file mode 100644 index 000000000..8050c2d1e --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/power/DeviceIdleReceiver.java @@ -0,0 +1,33 @@ +package com.fsck.k9.power; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.PowerManager; +import android.support.annotation.RequiresApi; + +import com.fsck.k9.service.MailService; +import timber.log.Timber; + + +@RequiresApi(api = Build.VERSION_CODES.M) +class DeviceIdleReceiver extends BroadcastReceiver { + private final PowerManager powerManager; + + + DeviceIdleReceiver(PowerManager powerManager) { + this.powerManager = powerManager; + } + + @Override + public void onReceive(Context context, Intent intent) { + boolean deviceInIdleMode = powerManager.isDeviceIdleMode(); + Timber.v("Device idle mode changed. Idle: %b", deviceInIdleMode); + + if (!deviceInIdleMode) { + MailService.actionReset(context, null); + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/power/DozeChecker.java b/k9mail/src/main/java/com/fsck/k9/power/DozeChecker.java new file mode 100644 index 000000000..7953dbb13 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/power/DozeChecker.java @@ -0,0 +1,28 @@ +package com.fsck.k9.power; + + +import android.content.Context; +import android.os.Build; +import android.os.PowerManager; +import android.support.annotation.RequiresApi; + + +public class DozeChecker { + private final PowerManager powerManager; + private final String packageName; + + + public DozeChecker(Context context) { + powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + packageName = context.getPackageName(); + } + + public boolean isDeviceIdleModeSupported() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public boolean isAppWhitelisted() { + return powerManager.isIgnoringBatteryOptimizations(packageName); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/helper/K9AlarmManagerTest.java b/k9mail/src/test/java/com/fsck/k9/helper/K9AlarmManagerTest.java index 037859723..29c827d85 100644 --- a/k9mail/src/test/java/com/fsck/k9/helper/K9AlarmManagerTest.java +++ b/k9mail/src/test/java/com/fsck/k9/helper/K9AlarmManagerTest.java @@ -1,13 +1,10 @@ package com.fsck.k9.helper; -import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.PendingIntent; -import android.content.Context; -import android.os.Build.VERSION_CODES; -import android.os.PowerManager; +import com.fsck.k9.power.DozeChecker; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -19,7 +16,6 @@ import static org.mockito.Mockito.when; public class K9AlarmManagerTest { - private static final String PACKAGE_NAME = "org.example.package"; private static final int TIMER_TYPE = AlarmManager.RTC_WAKEUP; private static final long TIMEOUT = 15L * 60L * 1000L; private static final PendingIntent PENDING_INTENT = createDummyPendingIntent(); @@ -27,16 +23,21 @@ public class K9AlarmManagerTest { @Mock private AlarmManager systemAlarmManager; + @Mock + private DozeChecker dozeChecker; + + private K9AlarmManager alarmManager; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + alarmManager = new K9AlarmManager(systemAlarmManager, dozeChecker); } @Test public void set_withoutDozeSupport_shouldCallSetOnAlarmManager() throws Exception { - K9AlarmManager alarmManager = createK9AlarmManagerWithoutDozeSupport(); + configureDozeSupport(false); alarmManager.set(TIMER_TYPE, TIMEOUT, PENDING_INTENT); @@ -45,27 +46,27 @@ public class K9AlarmManagerTest { @Test public void set_withDozeSupportAndNotWhiteListed_shouldCallSetOnAlarmManager() throws Exception { - K9AlarmManager alarmManager = createK9AlarmManagerWithDozeSupport(false); + configureDozeSupport(true); alarmManager.set(TIMER_TYPE, TIMEOUT, PENDING_INTENT); verify(systemAlarmManager).set(TIMER_TYPE, TIMEOUT, PENDING_INTENT); } - @TargetApi(VERSION_CODES.M) @Test public void set_withDozeSupportAndWhiteListed_shouldCallSetAndAllowWhileIdleOnAlarmManager() throws Exception { - K9AlarmManager alarmManager = createK9AlarmManagerWithDozeSupport(true); + configureDozeSupport(true); + addAppToBatteryOptimizationWhitelist(); alarmManager.set(TIMER_TYPE, TIMEOUT, PENDING_INTENT); verify(systemAlarmManager).setAndAllowWhileIdle(TIMER_TYPE, TIMEOUT, PENDING_INTENT); } - @TargetApi(VERSION_CODES.M) @Test public void cancel_shouldCallCancelOnAlarmManager() throws Exception { - K9AlarmManager alarmManager = createK9AlarmManagerWithDozeSupport(true); + configureDozeSupport(true); + addAppToBatteryOptimizationWhitelist(); alarmManager.cancel(PENDING_INTENT); @@ -73,54 +74,15 @@ public class K9AlarmManagerTest { } - private K9AlarmManager createK9AlarmManagerWithDozeSupport(boolean whiteListed) { - PowerManager powerManager = createPowerManager(whiteListed); - Context context = createContext(powerManager); - - return new TestK9AlarmManager(context, true); + private void configureDozeSupport(boolean supported) { + when(dozeChecker.isDeviceIdleModeSupported()).thenReturn(supported); } - private K9AlarmManager createK9AlarmManagerWithoutDozeSupport() { - PowerManager powerManager = mock(PowerManager.class); - Context context = createContext(powerManager); - - return new TestK9AlarmManager(context, false); - } - - @TargetApi(VERSION_CODES.M) - private PowerManager createPowerManager(boolean whiteListed) { - PowerManager powerManager = mock(PowerManager.class); - when(powerManager.isIgnoringBatteryOptimizations(PACKAGE_NAME)).thenReturn(whiteListed); - - return powerManager; - } - - private Context createContext(PowerManager powerManager) { - Context context = mock(Context.class); - when(context.getPackageName()).thenReturn(PACKAGE_NAME); - when(context.getSystemService(Context.ALARM_SERVICE)).thenReturn(systemAlarmManager); - when(context.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager); - - return context; + private void addAppToBatteryOptimizationWhitelist() { + when(dozeChecker.isAppWhitelisted()).thenReturn(true); } private static PendingIntent createDummyPendingIntent() { return mock(PendingIntent.class); } - - - class TestK9AlarmManager extends K9AlarmManager { - private final boolean dozeSupported; - - - TestK9AlarmManager(Context context, boolean dozeSupported) { - super(context); - this.dozeSupported = dozeSupported; - } - - @Override - protected boolean isDozeSupported() { - return dozeSupported; - } - } }