diff --git a/res/values/strings.xml b/res/values/strings.xml index fcc0b6d36bf..c58f6bb3b30 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4630,6 +4630,12 @@ App\'s background activity is limited when not in use App not allowed to run in background + + App can not be optimized for battery use + + Limit background activity? + + If you limit background activity for an app, it may misbehave Screen usage since full charge diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index 0142e629fa1..66a0ca2ee89 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -68,7 +68,8 @@ import java.util.List; public class AdvancedPowerUsageDetail extends DashboardFragment implements ButtonActionDialogFragment.AppButtonsDialogListener, AnomalyDialogFragment.AnomalyDialogListener, - LoaderManager.LoaderCallbacks> { + LoaderManager.LoaderCallbacks>, + BackgroundActivityPreferenceController.WarningConfirmationListener { public static final String TAG = "AdvancedPowerUsageDetail"; public static final String EXTRA_UID = "extra_uid"; @@ -109,6 +110,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements @VisibleForTesting AnomalySummaryPreferenceController mAnomalySummaryPreferenceController; private AppButtonsPreferenceController mAppButtonsPreferenceController; + private BackgroundActivityPreferenceController mBackgroundActivityPreferenceController; private DevicePolicyManagerWrapper mDpm; private UserManager mUserManager; @@ -319,7 +321,9 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements final int uid = bundle.getInt(EXTRA_UID, 0); final String packageName = bundle.getString(EXTRA_PACKAGE_NAME); - controllers.add(new BackgroundActivityPreferenceController(context, uid)); + mBackgroundActivityPreferenceController = new BackgroundActivityPreferenceController( + context, this, uid, packageName); + controllers.add(mBackgroundActivityPreferenceController); controllers.add(new BatteryOptimizationPreferenceController( (SettingsActivity) getActivity(), this, packageName)); mAppButtonsPreferenceController = new AppButtonsPreferenceController( @@ -364,4 +368,10 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements public void onLoaderReset(Loader> loader) { } + + @Override + public void onLimitBackgroundActivity() { + mBackgroundActivityPreferenceController.setUnchecked( + findPreference(mBackgroundActivityPreferenceController.getPreferenceKey())); + } } diff --git a/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java b/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java index 4d1cf77c2eb..cea6d165a74 100644 --- a/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java @@ -14,12 +14,17 @@ package com.android.settings.fuelgauge; +import android.app.AlertDialog; import android.app.AppOpsManager; +import android.app.Dialog; +import android.app.Fragment; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Bundle; import android.os.UserManager; import android.support.annotation.VisibleForTesting; import android.support.v14.preference.SwitchPreference; @@ -29,6 +34,7 @@ import android.util.Log; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.enterprise.DevicePolicyManagerWrapper; import com.android.settings.enterprise.DevicePolicyManagerWrapperImpl; import com.android.settingslib.core.AbstractPreferenceController; @@ -45,54 +51,72 @@ public class BackgroundActivityPreferenceController extends AbstractPreferenceCo private final PackageManager mPackageManager; private final AppOpsManager mAppOpsManager; private final UserManager mUserManager; - private final String[] mPackages; private final int mUid; @VisibleForTesting DevicePolicyManagerWrapper mDpm; - + private Fragment mFragment; private String mTargetPackage; + private boolean mIsPreOApp; + private PowerWhitelistBackend mPowerWhitelistBackend; - public BackgroundActivityPreferenceController(Context context, int uid) { + public BackgroundActivityPreferenceController(Context context, Fragment fragment, + int uid, String packageName) { + this(context, fragment, uid, packageName, PowerWhitelistBackend.getInstance()); + } + + @VisibleForTesting + BackgroundActivityPreferenceController(Context context, Fragment fragment, + int uid, String packageName, PowerWhitelistBackend backend) { super(context); + mPowerWhitelistBackend = backend; mPackageManager = context.getPackageManager(); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mDpm = new DevicePolicyManagerWrapperImpl( (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE)); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mUid = uid; - mPackages = mPackageManager.getPackagesForUid(mUid); + mFragment = fragment; + mTargetPackage = packageName; + mIsPreOApp = isLegacyApp(packageName); } @Override public void updateState(Preference preference) { final int mode = mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, mUid, mTargetPackage); + .checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mTargetPackage); + final boolean whitelisted = mPowerWhitelistBackend.isWhitelisted(mTargetPackage); // Set checked or not before we may set it disabled if (mode != AppOpsManager.MODE_ERRORED) { - final boolean checked = mode != AppOpsManager.MODE_IGNORED; + final boolean checked = whitelisted || mode != AppOpsManager.MODE_IGNORED; ((SwitchPreference) preference).setChecked(checked); } - if (mode == AppOpsManager.MODE_ERRORED + if (whitelisted || mode == AppOpsManager.MODE_ERRORED || Utils.isProfileOrDeviceOwner(mUserManager, mDpm, mTargetPackage)) { preference.setEnabled(false); + } else { + preference.setEnabled(true); } - updateSummary(preference); } @Override public boolean isAvailable() { - if (mPackages == null) { - return false; - } - for (final String packageName : mPackages) { - if (isLegacyApp(packageName)) { - mTargetPackage = packageName; - return true; - } - } + return mTargetPackage != null; + } - return false; + /** + * Called from the warning dialog, if the user decides to go ahead and disable background + * activity for this package + */ + public void setUnchecked(Preference preference) { + if (mIsPreOApp) { + mAppOpsManager.setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, mUid, mTargetPackage, + AppOpsManager.MODE_IGNORED); + } + mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mTargetPackage, + AppOpsManager.MODE_IGNORED); + ((SwitchPreference) preference).setChecked(false); + updateSummary(preference); } @Override @@ -102,19 +126,23 @@ public class BackgroundActivityPreferenceController extends AbstractPreferenceCo @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean switchOn = (Boolean) newValue; - mAppOpsManager.setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, mUid, mTargetPackage, - switchOn ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); - + final boolean switchOn = (Boolean) newValue; + if (!switchOn) { + final WarningDialogFragment dialogFragment = new WarningDialogFragment(); + dialogFragment.setTargetFragment(mFragment, 0); + dialogFragment.show(mFragment.getFragmentManager(), TAG); + return false; + } + if (mIsPreOApp) { + mAppOpsManager.setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, mUid, mTargetPackage, + AppOpsManager.MODE_ALLOWED); + } + mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mTargetPackage, + AppOpsManager.MODE_ALLOWED); updateSummary(preference); return true; } - @VisibleForTesting - String getTargetPackage() { - return mTargetPackage; - } - @VisibleForTesting boolean isLegacyApp(final String packageName) { try { @@ -131,8 +159,12 @@ public class BackgroundActivityPreferenceController extends AbstractPreferenceCo @VisibleForTesting void updateSummary(Preference preference) { + if (mPowerWhitelistBackend.isWhitelisted(mTargetPackage)) { + preference.setSummary(R.string.background_activity_summary_whitelisted); + return; + } final int mode = mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, mUid, mTargetPackage); + .checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mTargetPackage); if (mode == AppOpsManager.MODE_ERRORED) { preference.setSummary(R.string.background_activity_summary_disabled); @@ -142,4 +174,37 @@ public class BackgroundActivityPreferenceController extends AbstractPreferenceCo : R.string.background_activity_summary_off); } } + + interface WarningConfirmationListener { + void onLimitBackgroundActivity(); + } + + /** + * Warning dialog to show to the user as turning off background activity can lead to + * apps misbehaving as their background task scheduling guarantees will no longer be honored. + */ + public static class WarningDialogFragment extends InstrumentedDialogFragment { + @Override + public int getMetricsCategory() { + // TODO (b/65494831): add metric id + return 0; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final WarningConfirmationListener listener = + (WarningConfirmationListener) getTargetFragment(); + return new AlertDialog.Builder(getContext()) + .setTitle(R.string.background_activity_warning_dialog_title) + .setMessage(R.string.background_activity_warning_dialog_text) + .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + listener.onLimitBackgroundActivity(); + } + }) + .setNegativeButton(R.string.dlg_cancel, null) + .create(); + } + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceControllerTest.java index 91f4a2b606e..86836f9f3f4 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceControllerTest.java @@ -16,14 +16,25 @@ package com.android.settings.fuelgauge; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.UserManager; import android.support.v14.preference.SwitchPreference; +import android.widget.Button; import com.android.settings.R; import com.android.settings.TestConfig; @@ -38,22 +49,17 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import org.robolectric.shadows.ShadowAlertDialog; +import org.robolectric.shadows.ShadowDialog; +import org.robolectric.util.FragmentTestUtil; @RunWith(RobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class BackgroundActivityPreferenceControllerTest { - private static final int UID_NORMAL = 1234; - private static final int UID_SPECIAL = 2345; + private static final int UID_LOW_SDK = 1234; + private static final int UID_HIGH_SDK = 3456; private static final String HIGH_SDK_PACKAGE = "com.android.package.high"; private static final String LOW_SDK_PACKAGE = "com.android.package.low"; - private static final String[] PACKAGES_NORMAL = {LOW_SDK_PACKAGE}; - private static final String[] PACKAGES_SPECIAL = {HIGH_SDK_PACKAGE, LOW_SDK_PACKAGE}; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @@ -71,6 +77,10 @@ public class BackgroundActivityPreferenceControllerTest { private DevicePolicyManager mDevicePolicyManager; @Mock private DevicePolicyManagerWrapper mDevicePolicyManagerWrapper; + @Mock + private AdvancedPowerUsageDetail mFragment; + @Mock + private PowerWhitelistBackend mPowerWhitelistBackend; private BackgroundActivityPreferenceController mController; private SwitchPreference mPreference; private Context mShadowContext; @@ -85,19 +95,19 @@ public class BackgroundActivityPreferenceControllerTest { when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn( mDevicePolicyManager); - when(mPackageManager.getPackagesForUid(UID_NORMAL)).thenReturn(PACKAGES_NORMAL); - when(mPackageManager.getPackagesForUid(UID_SPECIAL)).thenReturn(PACKAGES_SPECIAL); when(mPackageManager.getApplicationInfo(HIGH_SDK_PACKAGE, PackageManager.GET_META_DATA)) .thenReturn(mHighApplicationInfo); when(mPackageManager.getApplicationInfo(LOW_SDK_PACKAGE, PackageManager.GET_META_DATA)) .thenReturn(mLowApplicationInfo); + + when(mPowerWhitelistBackend.isWhitelisted(LOW_SDK_PACKAGE)).thenReturn(false); mHighApplicationInfo.targetSdkVersion = Build.VERSION_CODES.O; mLowApplicationInfo.targetSdkVersion = Build.VERSION_CODES.L; mPreference = new SwitchPreference(mShadowContext); - mController = spy(new BackgroundActivityPreferenceController(mContext, UID_NORMAL)); - mController.isAvailable(); + mController = spy(new BackgroundActivityPreferenceController( + mContext, mFragment, UID_LOW_SDK, LOW_SDK_PACKAGE, mPowerWhitelistBackend)); mController.mDpm = mDevicePolicyManagerWrapper; } @@ -105,49 +115,66 @@ public class BackgroundActivityPreferenceControllerTest { public void testOnPreferenceChange_TurnOnCheck_MethodInvoked() { mController.onPreferenceChange(mPreference, true); - verify(mAppOpsManager).setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, - mController.getTargetPackage(), AppOpsManager.MODE_ALLOWED); - verify(mController).updateSummary(mPreference); + verify(mAppOpsManager).setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE, AppOpsManager.MODE_ALLOWED); + verify(mAppOpsManager).setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE, AppOpsManager.MODE_ALLOWED); + + assertThat(mPreference.getSummary()) + .isEqualTo(mShadowContext.getText(R.string.background_activity_summary_on)); } @Test - public void testOnPreferenceChange_TurnOffCheck_MethodInvoked() { - mController.onPreferenceChange(mPreference, false); - - verify(mAppOpsManager).setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, - mController.getTargetPackage(), AppOpsManager.MODE_IGNORED); - verify(mController).updateSummary(mPreference); + public void testOnPreferenceChange_TurnOnCheckHighSDK_MethodInvoked() { + mController = new BackgroundActivityPreferenceController(mContext, mFragment, UID_HIGH_SDK, + HIGH_SDK_PACKAGE, mPowerWhitelistBackend); + mController.onPreferenceChange(mPreference, true); + verify(mAppOpsManager).setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_HIGH_SDK, + HIGH_SDK_PACKAGE, AppOpsManager.MODE_ALLOWED); + verify(mAppOpsManager, never()).setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_HIGH_SDK, + HIGH_SDK_PACKAGE, AppOpsManager.MODE_ALLOWED); + assertThat(mPreference.getSummary()) + .isEqualTo(mShadowContext.getText(R.string.background_activity_summary_on)); } @Test public void testUpdateState_CheckOn_SetCheckedTrue() { - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_DEFAULT); + when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE)).thenReturn(AppOpsManager.MODE_ALLOWED); mController.updateState(mPreference); assertThat(mPreference.isChecked()).isTrue(); + assertThat(mPreference.isEnabled()).isTrue(); verify(mController).updateSummary(mPreference); } @Test public void testUpdateState_CheckOff_SetCheckedFalse() { - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_IGNORED); + when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE)).thenReturn(AppOpsManager.MODE_IGNORED); mController.updateState(mPreference); assertThat(mPreference.isChecked()).isFalse(); + assertThat(mPreference.isEnabled()).isTrue(); verify(mController).updateSummary(mPreference); } + @Test + public void testUpdateState_whitelisted() { + when(mPowerWhitelistBackend.isWhitelisted(LOW_SDK_PACKAGE)).thenReturn(true); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isTrue(); + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.getSummary()).isEqualTo( + mShadowContext.getText(R.string.background_activity_summary_whitelisted)); + } + @Test public void testUpdateSummary_modeError_showSummaryDisabled() { - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_ERRORED); + when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE)).thenReturn(AppOpsManager.MODE_ERRORED); final CharSequence expectedSummary = mShadowContext.getText( R.string.background_activity_summary_disabled); mController.updateSummary(mPreference); @@ -157,9 +184,8 @@ public class BackgroundActivityPreferenceControllerTest { @Test public void testUpdateSummary_modeDefault_showSummaryOn() { - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_DEFAULT); + when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE)).thenReturn(AppOpsManager.MODE_DEFAULT); final CharSequence expectedSummary = mShadowContext.getText( R.string.background_activity_summary_on); @@ -170,9 +196,8 @@ public class BackgroundActivityPreferenceControllerTest { @Test public void testUpdateSummary_modeIgnored_showSummaryOff() { - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_NORMAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_IGNORED); + when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID_LOW_SDK, + LOW_SDK_PACKAGE)).thenReturn(AppOpsManager.MODE_IGNORED); final CharSequence expectedSummary = mShadowContext.getText( R.string.background_activity_summary_off); @@ -182,31 +207,30 @@ public class BackgroundActivityPreferenceControllerTest { } @Test - public void testIsPackageAvailable_SdkLowerThanO_ReturnTrue() { + public void testIsLegacyApp_SdkLowerThanO_ReturnTrue() { assertThat(mController.isLegacyApp(LOW_SDK_PACKAGE)).isTrue(); } @Test - public void testIsPackageAvailable_SdkLargerOrEqualThanO_ReturnFalse() { + public void testIsLegacyApp_SdkLargerOrEqualThanO_ReturnFalse() { assertThat(mController.isLegacyApp(HIGH_SDK_PACKAGE)).isFalse(); } @Test - public void testMultiplePackages_ReturnStatusForTargetPackage() { - mController = new BackgroundActivityPreferenceController(mContext, UID_SPECIAL); - mController.mDpm = mDevicePolicyManagerWrapper; - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_SPECIAL, LOW_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_ALLOWED); - when(mAppOpsManager - .checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, UID_SPECIAL, HIGH_SDK_PACKAGE)) - .thenReturn(AppOpsManager.MODE_IGNORED); + public void testIsAvailable_ReturnTrue() { + assertThat(mController.isAvailable()).isTrue(); + } - final boolean available = mController.isAvailable(); - mController.updateState(mPreference); - - assertThat(available).isTrue(); - // Should get status from LOW_SDK_PACKAGE - assertThat(mPreference.isChecked()).isTrue(); + @Test + public void testWarningDialog() { + BackgroundActivityPreferenceController.WarningDialogFragment dialogFragment = + new BackgroundActivityPreferenceController.WarningDialogFragment(); + dialogFragment.setTargetFragment(mFragment, 0); + FragmentTestUtil.startFragment(dialogFragment); + final AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog(); + ShadowAlertDialog shadowDialog = shadowOf(dialog); + final Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + shadowDialog.clickOn(okButton.getId()); + verify(mFragment).onLimitBackgroundActivity(); } }