<item quantity="other">Limiting battery usage for %1$d apps</item>
</plurals>
+ <!-- Summary for restricted app to show the restriction time [CHAR LIMIT=NONE] -->
+ <string name="restricted_app_time_summary">Restricted <xliff:g id="time" example="5 days ago">%1$s</xliff:g></string>
+
<!-- Footer message for restrict app details page -->
<string name="restricted_app_detail_footer">These apps have been using battery in the background. Restricted apps may not work properly and notifications may be delayed.</string>
import com.android.internal.os.BatteryStatsHelper;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
+import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AnomalyInfo;
+import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.StatsManagerConfig;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.fuelgauge.PowerWhitelistBackend;
import com.android.settingslib.utils.PowerUtil;
+import com.android.settingslib.utils.ThreadUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
mContext = context;
mPackageManager = context.getPackageManager();
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
- mPowerUsageFeatureProvider = FeatureFactory.getFactory(
- context).getPowerUsageFeatureProvider(context);
+ mPowerUsageFeatureProvider = FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context);
}
public long getProcessTimeMs(@StatusType int type, @Nullable BatteryStats.Uid uid,
}
// Control whether app could run jobs in the background
mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName, mode);
+
+ ThreadUtils.postOnBackgroundThread(() -> {
+ final BatteryDatabaseManager batteryDatabaseManager = BatteryDatabaseManager
+ .getInstance(mContext);
+ if (mode == AppOpsManager.MODE_IGNORED) {
+ batteryDatabaseManager.insertAction(AnomalyDatabaseHelper.ActionType.RESTRICTION,
+ uid, packageName, System.currentTimeMillis());
+ } else if (mode == AppOpsManager.MODE_ALLOWED) {
+ batteryDatabaseManager.deleteAction(AnomalyDatabaseHelper.ActionType.RESTRICTION,
+ uid, packageName);
+ }
+ });
}
public boolean isForceAppStandbyEnabled(int uid, String packageName) {
import android.os.Bundle;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
+import android.util.Log;
+import android.util.SparseLongArray;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
+import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.BatteryTipDialogFragment;
import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
import com.android.settings.widget.AppCheckBoxPreference;
import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.FooterPreferenceMixinCompat;
import java.util.List;
@VisibleForTesting
static final String EXTRA_APP_INFO_LIST = "app_info_list";
private static final String KEY_PREF_RESTRICTED_APP_LIST = "restrict_app_list";
+ private static final long TIME_NULL = -1;
@VisibleForTesting
List<AppInfo> mAppInfos;
BatteryUtils mBatteryUtils;
@VisibleForTesting
PackageManager mPackageManager;
+ @VisibleForTesting
+ BatteryDatabaseManager mBatteryDatabaseManager;
private final FooterPreferenceMixinCompat mFooterPreferenceMixin =
new FooterPreferenceMixinCompat(this, getSettingsLifecycle());
mPackageManager = context.getPackageManager();
mIconDrawableFactory = IconDrawableFactory.newInstance(context);
mBatteryUtils = BatteryUtils.getInstance(context);
+ mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(context);
refreshUi();
}
void refreshUi() {
mRestrictedAppListGroup.removeAll();
final Context context = getPrefContext();
+ final SparseLongArray timestampArray = mBatteryDatabaseManager
+ .queryActionTime(AnomalyDatabaseHelper.ActionType.RESTRICTION);
+ final long now = System.currentTimeMillis();
for (int i = 0, size = mAppInfos.size(); i < size; i++) {
final CheckBoxPreference checkBoxPreference = new AppCheckBoxPreference(context);
return false;
});
+
+ final long timestamp = timestampArray.get(appInfo.uid, TIME_NULL);
+ if (timestamp != TIME_NULL) {
+ checkBoxPreference.setSummary(getString(R.string.restricted_app_time_summary,
+ StringUtil.formatRelativeTime(context, now - timestamp, false)));
+ }
+ final CharSequence test = checkBoxPreference.getSummaryOn();
mRestrictedAppListGroup.addPreference(checkBoxPreference);
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "Can't find package: " + appInfo.packageName);
}
}
}
package com.android.settings.fuelgauge.batterytip;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE;
+import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE;
+
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns
.ANOMALY_STATE;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns
.TIME_STAMP_MS;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.UID;
+import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ACTION;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ANOMALY;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.SparseLongArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.ActionColumns;
+
import androidx.annotation.VisibleForTesting;
/**
}
}
}
+
+ /**
+ * Query latest timestamps when an app has been performed action {@code type}
+ *
+ * @param type of action been performed
+ * @return {@link SparseLongArray} where key is uid and value is timestamp
+ */
+ public synchronized SparseLongArray queryActionTime(
+ @AnomalyDatabaseHelper.ActionType int type) {
+ final SparseLongArray timeStamps = new SparseLongArray();
+ try (SQLiteDatabase db = mDatabaseHelper.getReadableDatabase()) {
+ final String[] projection = {ActionColumns.UID, ActionColumns.TIME_STAMP_MS};
+ final String selection = ActionColumns.ACTION_TYPE + " = ? ";
+ final String[] selectionArgs = new String[]{String.valueOf(type)};
+
+ try (Cursor cursor = db.query(TABLE_ACTION, projection, selection, selectionArgs,
+ null /* groupBy */, null /* having */, null /* orderBy */)) {
+ final int uidIndex = cursor.getColumnIndex(ActionColumns.UID);
+ final int timestampIndex = cursor.getColumnIndex(ActionColumns.TIME_STAMP_MS);
+
+ while (cursor.moveToNext()) {
+ final int uid = cursor.getInt(uidIndex);
+ final long timeStamp = cursor.getLong(timestampIndex);
+ timeStamps.append(uid, timeStamp);
+ }
+ }
+ }
+
+ return timeStamps;
+ }
+
+ /**
+ * Insert an action, or update it if already existed
+ */
+ public synchronized boolean insertAction(@AnomalyDatabaseHelper.ActionType int type,
+ int uid, String packageName, long timestampMs) {
+ try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) {
+ final ContentValues values = new ContentValues();
+ values.put(ActionColumns.UID, uid);
+ values.put(ActionColumns.PACKAGE_NAME, packageName);
+ values.put(ActionColumns.ACTION_TYPE, type);
+ values.put(ActionColumns.TIME_STAMP_MS, timestampMs);
+ return db.insertWithOnConflict(TABLE_ACTION, null, values, CONFLICT_REPLACE) != -1;
+ }
+ }
+
+ /**
+ * Remove an action
+ */
+ public synchronized boolean deleteAction(@AnomalyDatabaseHelper.ActionType int type,
+ int uid, String packageName) {
+ try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) {
+ final String where =
+ ActionColumns.ACTION_TYPE + " = ? AND " + ActionColumns.UID + " = ? AND "
+ + ActionColumns.PACKAGE_NAME + " = ? ";
+ final String[] whereArgs = new String[]{String.valueOf(type), String.valueOf(uid),
+ String.valueOf(packageName)};
+
+ return db.delete(TABLE_ACTION, where, whereArgs) != 0;
+ }
+ }
}
import android.content.Context;
import android.text.format.DateUtils;
+import android.util.SparseLongArray;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
}
@Test
- public void testAllFunctions() {
+ public void allAnomalyFunctions() {
mBatteryDatabaseManager.insertAnomaly(UID_NEW, PACKAGE_NAME_NEW, TYPE_NEW,
AnomalyDatabaseHelper.State.NEW, NOW);
mBatteryDatabaseManager.insertAnomaly(UID_OLD, PACKAGE_NAME_OLD, TYPE_OLD,
}
@Test
- public void testUpdateAnomalies_updateSuccessfully() {
+ public void updateAnomalies_updateSuccessfully() {
mBatteryDatabaseManager.insertAnomaly(UID_NEW, PACKAGE_NAME_NEW, TYPE_NEW,
AnomalyDatabaseHelper.State.NEW, NOW);
mBatteryDatabaseManager.insertAnomaly(UID_OLD, PACKAGE_NAME_OLD, TYPE_OLD,
}
@Test
- public void testQueryAnomalies_removeDuplicateByUid() {
+ public void queryAnomalies_removeDuplicateByUid() {
mBatteryDatabaseManager.insertAnomaly(UID_NEW, PACKAGE_NAME_NEW, TYPE_NEW,
AnomalyDatabaseHelper.State.NEW, NOW);
mBatteryDatabaseManager.insertAnomaly(UID_NEW, PACKAGE_NAME_NEW, TYPE_OLD,
AnomalyDatabaseHelper.State.NEW);
assertThat(newAppInfos).containsExactly(mCombinedAppInfo);
}
+
+ @Test
+ public void allActionFunctions() {
+ final long timestamp = System.currentTimeMillis();
+ mBatteryDatabaseManager.insertAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, UID_OLD,
+ PACKAGE_NAME_OLD, 0);
+ mBatteryDatabaseManager.insertAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, UID_OLD,
+ PACKAGE_NAME_OLD, 1);
+ mBatteryDatabaseManager.insertAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, UID_NEW,
+ PACKAGE_NAME_NEW, timestamp);
+
+ final SparseLongArray timeArray = mBatteryDatabaseManager.queryActionTime(
+ AnomalyDatabaseHelper.ActionType.RESTRICTION);
+ assertThat(timeArray.size()).isEqualTo(2);
+ assertThat(timeArray.get(UID_OLD)).isEqualTo(1);
+ assertThat(timeArray.get(UID_NEW)).isEqualTo(timestamp);
+
+ mBatteryDatabaseManager.deleteAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, UID_NEW,
+ PACKAGE_NAME_NEW);
+ final SparseLongArray recentTimeArray = mBatteryDatabaseManager.queryActionTime(
+ AnomalyDatabaseHelper.ActionType.RESTRICTION);
+ assertThat(recentTimeArray.size()).isEqualTo(1);
+ assertThat(timeArray.get(UID_OLD)).isEqualTo(1);
+ }
}
import com.android.internal.os.BatterySipper;
import com.android.internal.os.BatteryStatsHelper;
+import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AnomalyInfo;
+import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
import com.android.settingslib.fuelgauge.PowerWhitelistBackend;
import org.junit.Before;
private ApplicationInfo mLowApplicationInfo;
@Mock
private PowerWhitelistBackend mPowerWhitelistBackend;
+ @Mock
+ private BatteryDatabaseManager mBatteryDatabaseManager;
private AnomalyInfo mAnomalyInfo;
private BatteryUtils mBatteryUtils;
private FakeFeatureFactory mFeatureFactory;
.thenReturn(TOTAL_BATTERY_USAGE + BATTERY_SCREEN_USAGE);
when(mBatteryStatsHelper.getStats().getDischargeAmount(anyInt()))
.thenReturn(DISCHARGE_AMOUNT);
+ BatteryDatabaseManager.setUpForTest(mBatteryDatabaseManager);
+ ShadowThreadUtils.setIsMainThread(true);
}
@Test
}
@Test
+ public void testSetForceAppStandby_restrictApp_recordTime() {
+ mBatteryUtils.setForceAppStandby(UID, HIGH_SDK_PACKAGE, AppOpsManager.MODE_IGNORED);
+
+ verify(mBatteryDatabaseManager).insertAction(
+ eq(AnomalyDatabaseHelper.ActionType.RESTRICTION), eq(UID),
+ eq(HIGH_SDK_PACKAGE), anyLong());
+ }
+
+ @Test
+ public void testSetForceAppStandby_unrestrictApp_deleteTime() {
+ mBatteryUtils.setForceAppStandby(UID, HIGH_SDK_PACKAGE, AppOpsManager.MODE_ALLOWED);
+
+ verify(mBatteryDatabaseManager).deleteAction(AnomalyDatabaseHelper.ActionType.RESTRICTION,
+ UID, HIGH_SDK_PACKAGE);
+ }
+
+ @Test
public void testIsForceAppStandbyEnabled_enabled_returnTrue() {
when(mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, UID,
PACKAGE_NAME)).thenReturn(AppOpsManager.MODE_IGNORED);
import android.os.Bundle;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
+import android.util.SparseLongArray;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
+import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
+import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.BatteryTipDialogFragment;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.CheckBoxPreference;
private IconDrawableFactory mIconDrawableFactory;
@Mock
private InstrumentedPreferenceFragment mFragment;
+ @Mock
+ private BatteryDatabaseManager mBatteryDatabaseManager;
private PreferenceManager mPreferenceManager;
private RestrictedAppDetails mRestrictedAppDetails;
private Context mContext;
doReturn(mPreferenceManager).when(mRestrictedAppDetails).getPreferenceManager();
doReturn(mContext).when(mFragment).getContext();
+ doReturn(mContext).when(mRestrictedAppDetails).getContext();
mRestrictedAppDetails.mPackageManager = mPackageManager;
mRestrictedAppDetails.mIconDrawableFactory = mIconDrawableFactory;
mRestrictedAppDetails.mAppInfos = new ArrayList<>();
mRestrictedAppDetails.mAppInfos.add(mAppInfo);
mRestrictedAppDetails.mRestrictedAppListGroup = spy(new PreferenceCategory(mContext));
mRestrictedAppDetails.mBatteryUtils = spy(new BatteryUtils(mContext));
+ mRestrictedAppDetails.mBatteryDatabaseManager = mBatteryDatabaseManager;
doReturn(mPreferenceManager).when(
mRestrictedAppDetails.mRestrictedAppListGroup).getPreferenceManager();
doReturn(APP_NAME).when(mPackageManager).getApplicationLabel(mApplicationInfo);
doReturn(true).when(mRestrictedAppDetails.mBatteryUtils).isForceAppStandbyEnabled(UID,
PACKAGE_NAME);
+ final SparseLongArray timestampArray = new SparseLongArray();
+ timestampArray.put(UID, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(5));
+ doReturn(timestampArray).when(mBatteryDatabaseManager)
+ .queryActionTime(AnomalyDatabaseHelper.ActionType.RESTRICTION);
mRestrictedAppDetails.refreshUi();
(CheckBoxPreference) mRestrictedAppDetails.mRestrictedAppListGroup.getPreference(0);
assertThat(preference.getTitle()).isEqualTo(APP_NAME);
assertThat(preference.isChecked()).isTrue();
+ assertThat(preference.getSummary()).isEqualTo("Restricted 5 hours ago");
}
@Test