resolveInfos = mPackageManager.queryIntentServices(intent,
- PackageManager.MATCH_DISABLED_COMPONENTS);
+ PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_META_DATA);
for (ResolveInfo info : resolveInfos) {
ServiceInfo sInfo = info.serviceInfo;
+
+ // Check if the tile requires a flag. If it does, hide tile if flag is off.
+ if (sInfo.metaData != null) {
+ String flag = sInfo.metaData.getString(
+ DevelopmentTiles.META_DATA_REQUIRES_SYSTEM_PROPERTY);
+ if (flag != null) {
+ boolean enabled = SystemProperties.getBoolean(flag, false);
+ if (!enabled) {
+ // Flagged tile, flag is not enabled
+ continue;
+ }
+ }
+ }
+
final int enabledSetting = mPackageManager.getComponentEnabledSetting(
new ComponentName(sInfo.packageName, sInfo.name));
boolean checked = enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
diff --git a/src/com/android/settings/development/qstile/DevelopmentTiles.java b/src/com/android/settings/development/qstile/DevelopmentTiles.java
index 0483aea861f..bb5216cb8d0 100644
--- a/src/com/android/settings/development/qstile/DevelopmentTiles.java
+++ b/src/com/android/settings/development/qstile/DevelopmentTiles.java
@@ -58,6 +58,22 @@ import com.android.settingslib.development.DevelopmentSettingsEnabler;
import com.android.settingslib.development.SystemPropPoker;
public abstract class DevelopmentTiles extends TileService {
+
+ /**
+ * Meta-data for a development tile to declare a sysprop flag that needs to be enabled for
+ * the tile to be available.
+ *
+ * To define the flag, set this meta-data on the tile's manifest declaration.
+ *
+ * {@literal
+ *
+ * }
+ *
+ */
+ public static final String META_DATA_REQUIRES_SYSTEM_PROPERTY =
+ "com.android.settings.development.qstile.REQUIRES_SYSTEM_PROPERTY";
+
private static final String TAG = "DevelopmentTiles";
protected abstract boolean isEnabled();
@@ -476,4 +492,77 @@ public abstract class DevelopmentTiles extends TileService {
Settings.System.SHOW_TOUCHES, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
}
}
+
+ /**
+ * Tile to enable desktop mode
+ */
+ public static class DesktopMode extends DevelopmentTiles {
+
+ private static final int SETTING_VALUE_ON = 1;
+ private static final int SETTING_VALUE_OFF = 0;
+ private Context mContext;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = getApplicationContext();
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.DESKTOP_MODE, SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+ }
+
+ private boolean isDesktopModeFlagEnabled() {
+ return SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false);
+ }
+
+ private boolean isFreeformFlagEnabled() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, SETTING_VALUE_OFF)
+ == SETTING_VALUE_ON;
+ }
+
+ private boolean isCaptionOnShellEnabled() {
+ return SystemProperties.getBoolean("persist.wm.debug.caption_on_shell", false);
+ }
+
+ @Override
+ protected void setIsEnabled(boolean isEnabled) {
+ if (isEnabled) {
+ // Check that all required features are enabled
+ if (!isDesktopModeFlagEnabled()) {
+ closeShade();
+ showMessage(
+ "Enable 'Desktop Windowing Proto 1' from the Flag Flipper app");
+ return;
+ }
+ if (!isCaptionOnShellEnabled()) {
+ closeShade();
+ showMessage("Enable 'Captions in Shell' from the Flag Flipper app");
+ return;
+ }
+ if (!isFreeformFlagEnabled()) {
+ closeShade();
+ showMessage(
+ "Enable freeform windows from developer settings");
+ return;
+ }
+ }
+
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.DESKTOP_MODE,
+ isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
+ closeShade();
+ }
+
+ private void closeShade() {
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ private void showMessage(String message) {
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ }
+ }
}
diff --git a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
index f7dd85ab67a..d43b2541db7 100644
--- a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
+++ b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
@@ -19,6 +19,7 @@ package com.android.settings.deviceinfo;
import android.app.ActivityManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;
@@ -57,15 +58,34 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
private String mVolumeId;
private VolumeInfo mVolume;
+ private final View.OnClickListener mUnmountListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new UnmountTask(getActivity(), mVolume).execute();
+ }
+ };
private DiskInfo mDisk;
-
private UsageProgressBarPreference mSummary;
-
private Preference mMount;
private Preference mFormatPublic;
- private Preference mFormatPrivate;
private Button mUnmount;
+ private final StorageEventListener mStorageListener = new StorageEventListener() {
+ @Override
+ public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
+ if (Objects.equals(mVolume.getId(), vol.getId())) {
+ mVolume = vol;
+ update();
+ }
+ }
+ @Override
+ public void onVolumeRecordChanged(VolumeRecord rec) {
+ if (Objects.equals(mVolume.getFsUuid(), rec.getFsUuid())) {
+ mVolume = mStorageManager.findVolumeById(mVolumeId);
+ update();
+ }
+ }
+ };
private boolean mIsPermittedToAdopt;
private boolean isVolumeValid() {
@@ -120,10 +140,7 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
mUnmount = new Button(getActivity());
mUnmount.setText(R.string.storage_menu_unmount);
mUnmount.setOnClickListener(mUnmountListener);
- mFormatPublic = buildAction(R.string.storage_menu_format);
- if (mIsPermittedToAdopt) {
- mFormatPrivate = buildAction(R.string.storage_menu_format_private);
- }
+ mFormatPublic = buildAction(R.string.storage_menu_format_option);
}
@Override
@@ -176,9 +193,6 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
mUnmount.setVisibility(View.GONE);
}
addPreference(mFormatPublic);
- if (mDisk.isAdoptable() && mIsPermittedToAdopt) {
- addPreference(mFormatPrivate);
- }
}
private void addPreference(Preference pref) {
@@ -215,39 +229,14 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
@Override
public boolean onPreferenceTreeClick(Preference pref) {
+ final Intent intent = new Intent(getActivity(), StorageWizardInit.class);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
if (pref == mMount) {
new MountTask(getActivity(), mVolume).execute();
} else if (pref == mFormatPublic) {
- StorageWizardFormatConfirm.showPublic(getActivity(), mDisk.getId());
- } else if (pref == mFormatPrivate) {
- StorageWizardFormatConfirm.showPrivate(getActivity(), mDisk.getId());
+ startActivity(intent);
}
return super.onPreferenceTreeClick(pref);
}
-
- private final View.OnClickListener mUnmountListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- new UnmountTask(getActivity(), mVolume).execute();
- }
- };
-
- private final StorageEventListener mStorageListener = new StorageEventListener() {
- @Override
- public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
- if (Objects.equals(mVolume.getId(), vol.getId())) {
- mVolume = vol;
- update();
- }
- }
-
- @Override
- public void onVolumeRecordChanged(VolumeRecord rec) {
- if (Objects.equals(mVolume.getFsUuid(), rec.getFsUuid())) {
- mVolume = mStorageManager.findVolumeById(mVolumeId);
- update();
- }
- }
- };
-}
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deviceinfo/StorageWizardBase.java b/src/com/android/settings/deviceinfo/StorageWizardBase.java
index b1b956e5fb5..c39f3841299 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardBase.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardBase.java
@@ -41,17 +41,20 @@ import android.widget.TextView;
import androidx.fragment.app.FragmentActivity;
import com.android.settings.R;
-import com.android.settingslib.Utils;
+import com.android.settings.SetupWizardUtils;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.util.ThemeHelper;
+import com.google.android.setupdesign.util.ThemeResolver;
import java.text.NumberFormat;
import java.util.List;
import java.util.Objects;
public abstract class StorageWizardBase extends FragmentActivity {
+
private static final String TAG = "StorageWizardBase";
protected static final String EXTRA_FORMAT_FORGET_UUID = "format_forget_uuid";
@@ -70,6 +73,16 @@ public abstract class StorageWizardBase extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
+ boolean isDayNightThemeSupportedBySuW = ThemeHelper.isSetupWizardDayNightEnabled(this);
+ int sudTheme =
+ new ThemeResolver.Builder(ThemeResolver.getDefault())
+ .setDefaultTheme(ThemeHelper.getSuwDefaultTheme(this))
+ .setUseDayNight(true)
+ .build()
+ .resolve("", !isDayNightThemeSupportedBySuW);
+
+ this.setTheme(sudTheme);
+ ThemeHelper.trySetDynamicColor(this);
super.onCreate(savedInstanceState);
mStorage = getSystemService(StorageManager.class);
@@ -97,20 +110,20 @@ public abstract class StorageWizardBase extends FragmentActivity {
mFooterBarMixin = getGlifLayout().getMixin(FooterBarMixin.class);
mFooterBarMixin.setSecondaryButton(
- new FooterButton.Builder(this)
- .setText(R.string.wizard_back)
- .setListener(this::onNavigateBack)
- .setButtonType(FooterButton.ButtonType.OTHER)
- .setTheme(R.style.SudGlifButton_Secondary)
- .build()
+ new FooterButton.Builder(this)
+ .setText(R.string.wizard_back)
+ .setListener(this::onNavigateBack)
+ .setButtonType(FooterButton.ButtonType.OTHER)
+ .setTheme(R.style.SudGlifButton_Secondary)
+ .build()
);
mFooterBarMixin.setPrimaryButton(
- new FooterButton.Builder(this)
- .setText(R.string.wizard_next)
- .setListener(this::onNavigateNext)
- .setButtonType(FooterButton.ButtonType.NEXT)
- .setTheme(R.style.SudGlifButton_Primary)
- .build()
+ new FooterButton.Builder(this)
+ .setText(R.string.wizard_next)
+ .setListener(this::onNavigateNext)
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build()
);
mBack = mFooterBarMixin.getSecondaryButton();
mNext = mFooterBarMixin.getPrimaryButton();
@@ -149,7 +162,7 @@ public abstract class StorageWizardBase extends FragmentActivity {
protected void setCurrentProgress(int progress) {
getProgressBar().setProgress(progress);
((TextView) requireViewById(R.id.storage_wizard_progress_summary)).setText(
- NumberFormat.getPercentInstance().format((double) progress / 100));
+ NumberFormat.getPercentInstance().format((double) progress / 100));
}
protected void setHeaderText(int resId, CharSequence... args) {
@@ -167,14 +180,14 @@ public abstract class StorageWizardBase extends FragmentActivity {
protected void setAuxChecklist() {
final FrameLayout aux = requireViewById(R.id.storage_wizard_aux);
aux.addView(LayoutInflater.from(aux.getContext())
- .inflate(R.layout.storage_wizard_checklist, aux, false));
+ .inflate(R.layout.storage_wizard_checklist, aux, false));
aux.setVisibility(View.VISIBLE);
// Customize string based on disk
((TextView) aux.requireViewById(R.id.storage_wizard_migrate_v2_checklist_media))
- .setText(TextUtils.expandTemplate(
- getText(R.string.storage_wizard_migrate_v2_checklist_media),
- getDiskShortDescription()));
+ .setText(TextUtils.expandTemplate(
+ getText(R.string.storage_wizard_migrate_v2_checklist_media),
+ getDiskShortDescription()));
}
protected void setBackButtonText(int resId, CharSequence... args) {
@@ -198,7 +211,6 @@ public abstract class StorageWizardBase extends FragmentActivity {
protected void setIcon(int resId) {
final GlifLayout layout = getGlifLayout();
final Drawable icon = getDrawable(resId).mutate();
- icon.setTintList(Utils.getColorAccent(layout.getContext()));
layout.setIcon(icon);
}
@@ -250,14 +262,14 @@ public abstract class StorageWizardBase extends FragmentActivity {
final List vols = mStorage.getVolumes();
for (VolumeInfo vol : vols) {
if (Objects.equals(mDisk.getId(), vol.getDiskId()) && (vol.getType() == type)
- && (vol.getState() == VolumeInfo.STATE_MOUNTED)) {
+ && (vol.getState() == VolumeInfo.STATE_MOUNTED)) {
return vol;
}
}
if (--attempts > 0) {
Log.w(TAG, "Missing mounted volume of type " + type + " hosted by disk "
- + mDisk.getId() + "; trying again");
+ + mDisk.getId() + "; trying again");
SystemClock.sleep(250);
} else {
return null;
@@ -265,7 +277,8 @@ public abstract class StorageWizardBase extends FragmentActivity {
}
}
- protected @NonNull CharSequence getDiskDescription() {
+ protected @NonNull
+ CharSequence getDiskDescription() {
if (mDisk != null) {
return mDisk.getDescription();
} else if (mVolume != null) {
@@ -275,7 +288,8 @@ public abstract class StorageWizardBase extends FragmentActivity {
}
}
- protected @NonNull CharSequence getDiskShortDescription() {
+ protected @NonNull
+ CharSequence getDiskShortDescription() {
if (mDisk != null) {
return mDisk.getShortDescription();
} else if (mVolume != null) {
@@ -294,4 +308,4 @@ public abstract class StorageWizardBase extends FragmentActivity {
}
}
};
-}
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java
index 9c18a0da608..e2d8c7edddc 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java
@@ -84,15 +84,23 @@ public class StorageWizardFormatConfirm extends InstrumentedDialogFragment {
builder.setTitle(TextUtils.expandTemplate(
getText(R.string.storage_wizard_format_confirm_v2_title),
disk.getShortDescription()));
- builder.setMessage(TextUtils.expandTemplate(
- getText(R.string.storage_wizard_format_confirm_v2_body),
+ if (formatPrivate) {
+ builder.setMessage(TextUtils.expandTemplate(
+ getText(R.string.storage_wizard_format_confirm_v2_body),
+ disk.getDescription(),
+ disk.getShortDescription(),
+ disk.getShortDescription()));
+ } else {
+ builder.setMessage(TextUtils.expandTemplate(
+ getText(R.string.storage_wizard_format_confirm_v2_body_external),
disk.getDescription(),
disk.getShortDescription(),
disk.getShortDescription()));
+ }
builder.setNegativeButton(android.R.string.cancel, null);
builder.setPositiveButton(
- TextUtils.expandTemplate(getText(R.string.storage_wizard_format_confirm_v2_action),
+ TextUtils.expandTemplate(getText(R.string.storage_menu_format_option),
disk.getShortDescription()),
(dialog, which) -> {
final Intent intent = new Intent(context, StorageWizardFormatProgress.class);
@@ -104,4 +112,4 @@ public class StorageWizardFormatConfirm extends InstrumentedDialogFragment {
return builder.create();
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deviceinfo/StorageWizardInit.java b/src/com/android/settings/deviceinfo/StorageWizardInit.java
index 426395c24fe..8d049836b6b 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardInit.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardInit.java
@@ -18,21 +18,34 @@ package com.android.settings.deviceinfo;
import android.app.ActivityManager;
import android.app.settings.SettingsEnums;
-import android.content.Intent;
import android.os.Bundle;
import android.os.UserManager;
-import android.os.storage.DiskInfo;
-import android.os.storage.VolumeInfo;
+import android.text.Html;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.view.MotionEvent;
import android.view.View;
-import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
public class StorageWizardInit extends StorageWizardBase {
- private Button mInternal;
private boolean mIsPermittedToAdopt;
+ private boolean mPortable;
+
+ private ViewFlipper mFlipper;
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ savedInstanceState.putBoolean("IS_PORTABLE", mPortable);
+ super.onSaveInstanceState(savedInstanceState);
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -41,63 +54,143 @@ public class StorageWizardInit extends StorageWizardBase {
finish();
return;
}
- setContentView(R.layout.storage_wizard_init);
mIsPermittedToAdopt = UserManager.get(this).isAdminUser()
- && !ActivityManager.isUserAMonkey();
+ && !ActivityManager.isUserAMonkey();
- setHeaderText(R.string.storage_wizard_init_v2_title, getDiskShortDescription());
+ if (!mIsPermittedToAdopt) {
+ //Notify guest users as to why formatting is disallowed
+ Toast.makeText(getApplicationContext(),
+ R.string.storage_wizard_guest, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
- mInternal = requireViewById(R.id.storage_wizard_init_internal);
+ setContentView(R.layout.storage_wizard_init);
+ setupHyperlink();
+ mPortable = true;
- setBackButtonText(R.string.storage_wizard_init_v2_later);
- setNextButtonVisibility(View.INVISIBLE);
- if (!mDisk.isAdoptable()) {
- // If not adoptable, we only have one choice
- mInternal.setEnabled(false);
- onNavigateExternal(null);
- } else if (!mIsPermittedToAdopt) {
- // TODO: Show a message about why this is disabled for guest and
- // that only an admin user can adopt an sd card.
- mInternal.setEnabled(false);
+ mFlipper = (ViewFlipper) findViewById(R.id.viewFlipper);
+ if (savedInstanceState != null) {
+ mPortable = savedInstanceState.getBoolean("IS_PORTABLE");
+ }
+ if(mPortable) {
+ mFlipper.setDisplayedChild(0);
+ setHeaderText(R.string.storage_wizard_init_v2_external_title,
+ getDiskShortDescription());
+ setNextButtonText(R.string.storage_wizard_init_v2_external_action);
+ setBackButtonText(R.string.wizard_back_adoptable);
+ setNextButtonVisibility(View.VISIBLE);
+ if (!mDisk.isAdoptable()) {
+ setBackButtonVisibility(View.GONE);
+ }
+ }
+ else {
+ mFlipper.setDisplayedChild(1);
+ setHeaderText(R.string.storage_wizard_init_v2_internal_title,
+ getDiskShortDescription());
+ setNextButtonText(R.string.storage_wizard_init_v2_internal_action);
+ setBackButtonText(R.string.wizard_back_adoptable);
+ setNextButtonVisibility(View.VISIBLE);
}
}
@Override
- public void onNavigateBack(View view) {
- finish();
+ public void onNavigateBack(View v) {
+ if (!mIsPermittedToAdopt) {
+ // TODO: Show a message about why this is disabled for guest and
+ // that only an admin user can adopt an sd card.
+
+ v.setEnabled(false);
+ } else if (mPortable == false) {
+ mFlipper.showNext();
+ setHeaderText(R.string.storage_wizard_init_v2_external_title,
+ getDiskShortDescription());
+ setNextButtonText(R.string.storage_wizard_init_v2_external_action);
+ setBackButtonText(R.string.wizard_back_adoptable);
+ setBackButtonVisibility(View.VISIBLE);
+ mPortable = true;
+ } else {
+ mFlipper.showNext();
+ setHeaderText(R.string.storage_wizard_init_v2_internal_title,
+ getDiskShortDescription());
+ setNextButtonText(R.string.storage_wizard_init_v2_internal_action);
+ setBackButtonText(R.string.wizard_back_adoptable);
+ setBackButtonVisibility(View.VISIBLE);
+ mPortable = false;
+ }
+ }
+
+ @Override
+ public void onNavigateNext(View v) {
+ if (mPortable) {
+ onNavigateExternal(v);
+ } else {
+ onNavigateInternal(v);
+ }
}
public void onNavigateExternal(View view) {
if (view != null) {
// User made an explicit choice for external
FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this,
- SettingsEnums.ACTION_STORAGE_INIT_EXTERNAL);
- }
-
- if (mVolume != null && mVolume.getType() == VolumeInfo.TYPE_PUBLIC
- && mVolume.getState() != VolumeInfo.STATE_UNMOUNTABLE) {
- // Remember that user made decision
- mStorage.setVolumeInited(mVolume.getFsUuid(), true);
-
- final Intent intent = new Intent(this, StorageWizardReady.class);
- intent.putExtra(DiskInfo.EXTRA_DISK_ID, mDisk.getId());
- startActivity(intent);
- finish();
-
- } else {
- // Gotta format to get there
- StorageWizardFormatConfirm.showPublic(this, mDisk.getId());
+ SettingsEnums.ACTION_STORAGE_INIT_EXTERNAL);
}
+ StorageWizardFormatConfirm.showPublic(this, mDisk.getId());
}
public void onNavigateInternal(View view) {
if (view != null) {
// User made an explicit choice for internal
FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this,
- SettingsEnums.ACTION_STORAGE_INIT_INTERNAL);
+ SettingsEnums.ACTION_STORAGE_INIT_INTERNAL);
}
-
StorageWizardFormatConfirm.showPrivate(this, mDisk.getId());
}
-}
+
+ private void setupHyperlink() {
+ TextView external_storage_textview = findViewById(R.id.storage_wizard_init_external_text);
+ TextView internal_storage_textview = findViewById(R.id.storage_wizard_init_internal_text);
+ String external_storage_text = getResources().getString(R.string.
+ storage_wizard_init_v2_external_summary);
+ String internal_storage_text = getResources().getString(R.string.
+ storage_wizard_init_v2_internal_summary);
+
+ Spannable external_storage_spannable = styleFont(external_storage_text);
+ Spannable internal_storage_spannable = styleFont(internal_storage_text);
+ external_storage_textview.setText(external_storage_spannable);
+ internal_storage_textview.setText(internal_storage_spannable);
+
+ external_storage_textview.setMovementMethod(LinkMovementMethod.getInstance());
+ internal_storage_textview.setMovementMethod(LinkMovementMethod.getInstance());
+ external_storage_textview.setOnTouchListener(listener);
+ internal_storage_textview.setOnTouchListener(listener);
+ }
+
+ private Spannable styleFont(String text) {
+ Spannable s = (Spannable) Html.fromHtml(text);
+ for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
+ TypefaceSpan typefaceSpan = new TypefaceSpan("sans-serif-medium");
+ s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
+ }
+ return s;
+ }
+ private View.OnTouchListener listener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if(event.getAction() == MotionEvent.ACTION_UP) {
+ if (isInside(v, event)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isInside(View v, MotionEvent event) {
+ return !(event.getX() < 0 || event.getY() < 0
+ || event.getX() > v.getMeasuredWidth()
+ || event.getY() > v.getMeasuredHeight());
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deviceinfo/StorageWizardReady.java b/src/com/android/settings/deviceinfo/StorageWizardReady.java
index 813bcc6e201..8de94724afd 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardReady.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardReady.java
@@ -19,6 +19,7 @@ package com.android.settings.deviceinfo;
import android.os.Bundle;
import android.os.storage.VolumeInfo;
import android.view.View;
+import android.widget.ImageView;
import com.android.settings.R;
@@ -48,7 +49,9 @@ public class StorageWizardReady extends StorageWizardBase {
setBodyText(R.string.storage_wizard_ready_v2_external_body,
getDiskDescription());
}
-
+ ImageView img = (ImageView) findViewById(R.id.storage_wizard_body_image);
+ img.setImageResource(R.drawable.ic_storage_wizard_ready);
+ setIcon(R.drawable.ic_test_tick);
setNextButtonText(R.string.done);
setBackButtonVisibility(View.INVISIBLE);
}
diff --git a/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java
index 4b87e422c1b..289db520ec0 100644
--- a/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java
+++ b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java
@@ -30,6 +30,7 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
@@ -55,7 +56,9 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
OnPrepareOptionsMenu, OnOptionsItemSelected {
private static final String TAG = "VolumeOptionMenuController";
-
+ private final Context mContext;
+ private final Fragment mFragment;
+ private final PackageManager mPackageManager;
@VisibleForTesting
MenuItem mRename;
@VisibleForTesting
@@ -74,18 +77,12 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
MenuItem mFree;
@VisibleForTesting
MenuItem mForget;
-
- private final Context mContext;
- private final Fragment mFragment;
- private final PackageManager mPackageManager;
- private final StorageManager mStorageManager;
private StorageEntry mStorageEntry;
public VolumeOptionMenuController(Context context, Fragment parent, StorageEntry storageEntry) {
mContext = context;
mFragment = parent;
mPackageManager = context.getPackageManager();
- mStorageManager = context.getSystemService(StorageManager.class);
mStorageEntry = storageEntry;
}
@@ -162,12 +159,7 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
if (mStorageEntry.isPublic()) {
mRename.setVisible(true);
mUnmount.setVisible(true);
- mFormat.setVisible(true);
- final DiskInfo diskInfo = mStorageManager.findDiskById(mStorageEntry.getDiskId());
- mFormatAsInternal.setVisible(diskInfo != null
- && diskInfo.isAdoptable()
- && UserManager.get(mContext).isAdminUser()
- && !ActivityManager.isUserAMonkey());
+ mFormatAsInternal.setVisible(true);
return;
}
}
@@ -225,6 +217,16 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
}
if (menuId == R.id.storage_format_as_portable) {
if (mStorageEntry.isPrivate()) {
+ boolean mIsPermittedToAdopt = UserManager.get(mContext).isAdminUser()
+ && !ActivityManager.isUserAMonkey();
+
+ if(!mIsPermittedToAdopt){
+ //Notify guest users as to why formatting is disallowed
+ Toast.makeText(mFragment.getActivity(),
+ R.string.storage_wizard_guest,Toast.LENGTH_LONG).show();
+ (mFragment.getActivity()).finish();
+ return false;
+ }
final Bundle args = new Bundle();
args.putString(VolumeInfo.EXTRA_VOLUME_ID, mStorageEntry.getId());
new SubSettingLauncher(mContext)
@@ -239,8 +241,9 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
}
if (menuId == R.id.storage_format_as_internal) {
if (mStorageEntry.isPublic()) {
- StorageWizardFormatConfirm.showPrivate(mFragment.getActivity(),
- mStorageEntry.getDiskId());
+ final Intent intent = new Intent(mFragment.getActivity(), StorageWizardInit.class);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mStorageEntry.getId());
+ mContext.startActivity(intent);
return true;
}
return false;
@@ -269,4 +272,4 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp
updateOptionsMenu();
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
index f8816fe13a8..e6ac6069d9a 100644
--- a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java
@@ -98,7 +98,8 @@ public class FirmwareVersionDetailPreferenceController extends BasePreferenceCon
final Intent intent = new Intent(Intent.ACTION_MAIN)
.setClassName(
- "android", com.android.internal.app.PlatLogoActivity.class.getName());
+ "android", com.android.internal.app.PlatLogoActivity.class.getName())
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
mContext.startActivity(intent);
} catch (Exception e) {
diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java
index 3b08ae7f00b..7c4b3aeef03 100644
--- a/src/com/android/settings/display/ScreenResolutionFragment.java
+++ b/src/com/android/settings/display/ScreenResolutionFragment.java
@@ -19,14 +19,18 @@ package com.android.settings.display;
import static com.android.settings.display.ScreenResolutionController.FHD_WIDTH;
import static com.android.settings.display.ScreenResolutionController.QHD_WIDTH;
+import android.annotation.Nullable;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
+import android.provider.Settings;
import android.text.TextUtils;
import android.view.Display;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
@@ -34,6 +38,7 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.widget.RadioButtonPickerFragment;
+import com.android.settingslib.display.DisplayDensityUtils;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.CandidateInfo;
import com.android.settingslib.widget.FooterPreference;
@@ -44,22 +49,25 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
/** Preference fragment used for switch screen resolution */
@SearchIndexable
public class ScreenResolutionFragment extends RadioButtonPickerFragment {
-
private static final String TAG = "ScreenResolution";
private Resources mResources;
private static final int FHD_INDEX = 0;
private static final int QHD_INDEX = 1;
+ private static final String RESOLUTION_METRIC_SETTING_KEY = "user_selected_resolution";
private Display mDefaultDisplay;
private String[] mScreenResolutionOptions;
private Set mResolutions;
private String[] mScreenResolutionSummaries;
private IllustrationPreference mImagePreference;
+ private DisplayObserver mDisplayObserver;
+ private AccessibilityManager mAccessibilityManager;
@Override
public void onAttach(Context context) {
@@ -67,6 +75,7 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
mDefaultDisplay =
context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY);
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mResources = context.getResources();
mScreenResolutionOptions =
mResources.getStringArray(R.array.config_screen_resolution_options_strings);
@@ -74,6 +83,7 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
mResources.getStringArray(R.array.config_screen_resolution_summaries_strings);
mResolutions = getAllSupportedResolution();
mImagePreference = new IllustrationPreference(context);
+ mDisplayObserver = new DisplayObserver(context);
}
@Override
@@ -151,16 +161,35 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
/** Using display manager to set the display mode. */
@VisibleForTesting
- public void setDisplayMode(int width) {
+ public void setDisplayMode(final int width) {
+ mDisplayObserver.startObserve();
+
+ /** For store settings globally. */
+ /** TODO(b/238061217): Moving to an atom with the same string */
+ Settings.System.putString(
+ getContext().getContentResolver(),
+ RESOLUTION_METRIC_SETTING_KEY,
+ getPreferMode(width).getPhysicalWidth()
+ + "x"
+ + getPreferMode(width).getPhysicalHeight());
+
+ /** Apply the resolution change. */
mDefaultDisplay.setUserPreferredDisplayMode(getPreferMode(width));
}
/** Get the key corresponding to the resolution. */
@VisibleForTesting
String getKeyForResolution(int width) {
- return width == FHD_WIDTH ? mScreenResolutionOptions[FHD_INDEX]
- : width == QHD_WIDTH ? mScreenResolutionOptions[QHD_INDEX]
- : null;
+ return width == FHD_WIDTH
+ ? mScreenResolutionOptions[FHD_INDEX]
+ : width == QHD_WIDTH ? mScreenResolutionOptions[QHD_INDEX] : null;
+ }
+
+ /** Get the width corresponding to the resolution key. */
+ int getWidthForResoluitonKey(String key) {
+ return mScreenResolutionOptions[FHD_INDEX].equals(key)
+ ? FHD_WIDTH
+ : mScreenResolutionOptions[QHD_INDEX].equals(key) ? QHD_WIDTH : -1;
}
@Override
@@ -171,18 +200,36 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
}
@Override
- protected boolean setDefaultKey(String key) {
- if (mScreenResolutionOptions[FHD_INDEX].equals(key)) {
- setDisplayMode(FHD_WIDTH);
-
- } else if (mScreenResolutionOptions[QHD_INDEX].equals(key)) {
- setDisplayMode(QHD_WIDTH);
+ protected boolean setDefaultKey(final String key) {
+ int width = getWidthForResoluitonKey(key);
+ if (width < 0) {
+ return false;
}
+ setDisplayMode(width);
updateIllustrationImage(mImagePreference);
+
return true;
}
+ @Override
+ public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
+ String selectedKey = selected.getKey();
+ int selectedWidth = getWidthForResoluitonKey(selectedKey);
+ if (!mDisplayObserver.setPendingResolutionChange(selectedWidth)) {
+ return;
+ }
+
+ if (mAccessibilityManager.isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ event.getText().add(mResources.getString(R.string.screen_resolution_selected_a11y));
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ super.onRadioButtonClicked(selected);
+ }
+
/** Update the resolution image according display mode. */
private void updateIllustrationImage(IllustrationPreference preference) {
String key = getDefaultKey();
@@ -236,10 +283,6 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.screen_resolution_settings) {
-
- boolean mIsFHDSupport = false;
- boolean mIsQHDSupport = false;
-
@Override
protected boolean isPageSearchEnabled(Context context) {
ScreenResolutionController mController =
@@ -247,4 +290,110 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
return mController.checkSupportedResolutions();
}
};
+
+ private static final class DisplayObserver implements DisplayManager.DisplayListener {
+ private final @Nullable Context mContext;
+ private int mDefaultDensity;
+ private int mCurrentIndex;
+ private AtomicInteger mPreviousWidth = new AtomicInteger(-1);
+
+ DisplayObserver(Context context) {
+ mContext = context;
+ }
+
+ public void startObserve() {
+ if (mContext == null) {
+ return;
+ }
+
+ final DisplayDensityUtils density = new DisplayDensityUtils(mContext);
+ final int currentIndex = density.getCurrentIndex();
+ final int defaultDensity = density.getDefaultDensity();
+
+ if (density.getValues()[mCurrentIndex] == density.getDefaultDensity()) {
+ return;
+ }
+
+ mDefaultDensity = defaultDensity;
+ mCurrentIndex = currentIndex;
+ final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+ dm.registerDisplayListener(this, null);
+ }
+
+ public void stopObserve() {
+ if (mContext == null) {
+ return;
+ }
+
+ final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+ dm.unregisterDisplayListener(this);
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId != Display.DEFAULT_DISPLAY) {
+ return;
+ }
+
+ if (!isDensityChanged() || !isResolutionChangeApplied()) {
+ return;
+ }
+
+ restoreDensity();
+ stopObserve();
+ }
+
+ private void restoreDensity() {
+ final DisplayDensityUtils density = new DisplayDensityUtils(mContext);
+ if (density.getValues()[mCurrentIndex] != density.getDefaultDensity()) {
+ DisplayDensityUtils.setForcedDisplayDensity(
+ Display.DEFAULT_DISPLAY, density.getValues()[mCurrentIndex]);
+ }
+
+ mDefaultDensity = density.getDefaultDensity();
+ }
+
+ private boolean isDensityChanged() {
+ final DisplayDensityUtils density = new DisplayDensityUtils(mContext);
+ if (density.getDefaultDensity() == mDefaultDensity) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private int getCurrentWidth() {
+ final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+ return dm.getDisplay(Display.DEFAULT_DISPLAY).getMode().getPhysicalWidth();
+ }
+
+ private boolean setPendingResolutionChange(int selectedWidth) {
+ int currentWidth = getCurrentWidth();
+
+ if (selectedWidth == currentWidth) {
+ return false;
+ }
+ if (mPreviousWidth.get() != -1 && !isResolutionChangeApplied()) {
+ return false;
+ }
+
+ mPreviousWidth.set(currentWidth);
+
+ return true;
+ }
+
+ private boolean isResolutionChangeApplied() {
+ if (mPreviousWidth.get() == getCurrentWidth()) {
+ return false;
+ }
+
+ return true;
+ }
+ }
}
diff --git a/src/com/android/settings/display/ScreenSaverPreferenceController.java b/src/com/android/settings/display/ScreenSaverPreferenceController.java
index c1b0b4e9eb6..676a567f202 100644
--- a/src/com/android/settings/display/ScreenSaverPreferenceController.java
+++ b/src/com/android/settings/display/ScreenSaverPreferenceController.java
@@ -14,6 +14,7 @@
package com.android.settings.display;
import android.content.Context;
+import android.os.UserManager;
import androidx.preference.Preference;
@@ -32,8 +33,11 @@ public class ScreenSaverPreferenceController extends AbstractPreferenceControlle
@Override
public boolean isAvailable() {
- return mContext.getResources().getBoolean(
+ final boolean dreamsSupported = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_dreamsSupported);
+ final boolean dreamsOnlyEnabledForSystemUser = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser);
+ return dreamsSupported && (!dreamsOnlyEnabledForSystemUser || isSystemUser());
}
@Override
@@ -45,4 +49,9 @@ public class ScreenSaverPreferenceController extends AbstractPreferenceControlle
public void updateState(Preference preference) {
preference.setSummary(DreamSettings.getSummaryTextWithDreamName(mContext));
}
+
+ private boolean isSystemUser() {
+ final UserManager userManager = mContext.getSystemService(UserManager.class);
+ return userManager.isSystemUser();
+ }
}
diff --git a/src/com/android/settings/display/WallpaperPreferenceController.java b/src/com/android/settings/display/WallpaperPreferenceController.java
index 00687b2aaba..9972bf9cabe 100644
--- a/src/com/android/settings/display/WallpaperPreferenceController.java
+++ b/src/com/android/settings/display/WallpaperPreferenceController.java
@@ -41,6 +41,8 @@ public class WallpaperPreferenceController extends BasePreferenceController {
private final String mWallpaperPackage;
private final String mWallpaperClass;
private final String mStylesAndWallpaperClass;
+ private final String mWallpaperActionName;
+ private final String mStylesAndWallpaperActionName;
private final String mWallpaperLaunchExtra;
public WallpaperPreferenceController(Context context, String key) {
@@ -49,6 +51,9 @@ public class WallpaperPreferenceController extends BasePreferenceController {
mWallpaperClass = mContext.getString(R.string.config_wallpaper_picker_class);
mStylesAndWallpaperClass =
mContext.getString(R.string.config_styles_and_wallpaper_picker_class);
+ mWallpaperActionName = mContext.getString(R.string.config_wallpaper_picker_action);
+ mStylesAndWallpaperActionName =
+ mContext.getString(R.string.config_styles_and_wallpaper_picker_action);
mWallpaperLaunchExtra = mContext.getString(R.string.config_wallpaper_picker_launch_extra);
}
@@ -72,6 +77,10 @@ public class WallpaperPreferenceController extends BasePreferenceController {
return areStylesAvailable() ? mStylesAndWallpaperClass : mWallpaperClass;
}
+ public String getComponentActionName() {
+ return areStylesAvailable() ? mStylesAndWallpaperActionName : mWallpaperActionName;
+ }
+
public String getKeywords() {
StringBuilder sb = new StringBuilder(mContext.getString(R.string.keywords_wallpaper));
if (areStylesAvailable()) {
diff --git a/src/com/android/settings/dream/DreamAdapter.java b/src/com/android/settings/dream/DreamAdapter.java
index cfee12e0f42..b81d6b6d679 100644
--- a/src/com/android/settings/dream/DreamAdapter.java
+++ b/src/com/android/settings/dream/DreamAdapter.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
import android.text.TextUtils;
+import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -41,10 +42,9 @@ import java.util.List;
*/
public class DreamAdapter extends RecyclerView.Adapter {
private final List mItemList;
- @LayoutRes
- private final int mLayoutRes;
private int mLastSelectedPos = -1;
private boolean mEnabled = true;
+ private SparseIntArray mLayouts = new SparseIntArray();
/**
* View holder for each {@link IDreamItem}.
@@ -83,16 +83,6 @@ public class DreamAdapter extends RecyclerView.Adapter
mSummaryView.setVisibility(View.VISIBLE);
}
- final Drawable previewImage = item.getPreviewImage();
- if (previewImage != null) {
- mPreviewView.setImageDrawable(previewImage);
- mPreviewView.setClipToOutline(true);
- mPreviewPlaceholderView.setVisibility(View.GONE);
- } else {
- mPreviewView.setImageDrawable(null);
- mPreviewPlaceholderView.setVisibility(View.VISIBLE);
- }
-
final Drawable icon = item.isActive()
? mContext.getDrawable(R.drawable.ic_dream_check_circle)
: item.getIcon().mutate();
@@ -122,12 +112,24 @@ public class DreamAdapter extends RecyclerView.Adapter
itemView.setClickable(true);
}
- mCustomizeButton.setOnClickListener(v -> item.onCustomizeClicked());
- mCustomizeButton.setVisibility(
- item.allowCustomization() && mEnabled ? View.VISIBLE : View.GONE);
- // This must be called AFTER itemView.setSelected above, in order to keep the
- // customize button in an unselected state.
- mCustomizeButton.setSelected(false);
+ if (item.viewType() != DreamItemViewTypes.NO_DREAM_ITEM) {
+ final Drawable previewImage = item.getPreviewImage();
+ if (previewImage != null) {
+ mPreviewView.setImageDrawable(previewImage);
+ mPreviewView.setClipToOutline(true);
+ mPreviewPlaceholderView.setVisibility(View.GONE);
+ } else {
+ mPreviewView.setImageDrawable(null);
+ mPreviewPlaceholderView.setVisibility(View.VISIBLE);
+ }
+
+ mCustomizeButton.setOnClickListener(v -> item.onCustomizeClicked());
+ mCustomizeButton.setVisibility(
+ item.allowCustomization() && mEnabled ? View.VISIBLE : View.GONE);
+ // This must be called AFTER itemView.setSelected above, in order to keep the
+ // customize button in an unselected state.
+ mCustomizeButton.setSelected(false);
+ }
setEnabledStateOnViews(itemView, mEnabled);
}
@@ -149,16 +151,22 @@ public class DreamAdapter extends RecyclerView.Adapter
}
}
+ public DreamAdapter(SparseIntArray layouts, List itemList) {
+ mItemList = itemList;
+ mLayouts = layouts;
+ }
+
public DreamAdapter(@LayoutRes int layoutRes, List itemList) {
mItemList = itemList;
- mLayoutRes = layoutRes;
+ mLayouts.append(DreamItemViewTypes.DREAM_ITEM, layoutRes);
}
@NonNull
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+ @DreamItemViewTypes.ViewType int viewType) {
View view = LayoutInflater.from(viewGroup.getContext())
- .inflate(mLayoutRes, viewGroup, false);
+ .inflate(mLayouts.get(viewType), viewGroup, false);
return new DreamViewHolder(view, viewGroup.getContext());
}
@@ -167,6 +175,11 @@ public class DreamAdapter extends RecyclerView.Adapter
((DreamViewHolder) viewHolder).bindView(mItemList.get(i), i);
}
+ @Override
+ public @DreamItemViewTypes.ViewType int getItemViewType(int position) {
+ return mItemList.get(position).viewType();
+ }
+
@Override
public int getItemCount() {
return mItemList.size();
diff --git a/src/com/android/settings/dream/DreamComplicationPreferenceController.java b/src/com/android/settings/dream/DreamComplicationPreferenceController.java
index d9c4fb3512a..596fe20f2de 100644
--- a/src/com/android/settings/dream/DreamComplicationPreferenceController.java
+++ b/src/com/android/settings/dream/DreamComplicationPreferenceController.java
@@ -42,14 +42,12 @@ public class DreamComplicationPreferenceController extends TogglePreferenceContr
@Override
public boolean isChecked() {
- return mBackend.getEnabledComplications().containsAll(mBackend.getSupportedComplications());
+ return mBackend.getComplicationsEnabled();
}
@Override
public boolean setChecked(boolean isChecked) {
- for (int complication : mBackend.getSupportedComplications()) {
- mBackend.setComplicationEnabled(complication, isChecked);
- }
+ mBackend.setComplicationsEnabled(isChecked);
return true;
}
diff --git a/src/com/android/settings/dream/DreamItemViewTypes.java b/src/com/android/settings/dream/DreamItemViewTypes.java
new file mode 100644
index 00000000000..b7202429769
--- /dev/null
+++ b/src/com/android/settings/dream/DreamItemViewTypes.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.dream;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class representing a dream item view types.
+ */
+public final class DreamItemViewTypes {
+
+ /**
+ * The default dream item layout
+ */
+ public static final int DREAM_ITEM = 0;
+
+ /**
+ * The dream item layout indicating no dream item selected.
+ */
+ public static final int NO_DREAM_ITEM = 1;
+
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({DreamItemViewTypes.DREAM_ITEM, DreamItemViewTypes.NO_DREAM_ITEM})
+ public @interface ViewType {}
+}
diff --git a/src/com/android/settings/dream/DreamPickerController.java b/src/com/android/settings/dream/DreamPickerController.java
index 261db6c8ef8..f1a018f8b51 100644
--- a/src/com/android/settings/dream/DreamPickerController.java
+++ b/src/com/android/settings/dream/DreamPickerController.java
@@ -132,7 +132,7 @@ public class DreamPickerController extends BasePreferenceController {
mActiveDream = mDreamInfo;
mBackend.setActiveDream(mDreamInfo.componentName);
mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
- SettingsEnums.ACTION_DREAM_SELECT_TYPE, SettingsEnums.PAGE_UNKNOWN,
+ SettingsEnums.ACTION_DREAM_SELECT_TYPE, SettingsEnums.DREAM,
mDreamInfo.componentName.flattenToString(), 1);
}
diff --git a/src/com/android/settings/dream/IDreamItem.java b/src/com/android/settings/dream/IDreamItem.java
index 49c82bec42a..911a3cf0bd2 100644
--- a/src/com/android/settings/dream/IDreamItem.java
+++ b/src/com/android/settings/dream/IDreamItem.java
@@ -67,4 +67,11 @@ public interface IDreamItem {
default boolean allowCustomization() {
return false;
}
+
+ /**
+ * Returns whether or not this item is the no screensaver item.
+ */
+ default @DreamItemViewTypes.ViewType int viewType() {
+ return DreamItemViewTypes.DREAM_ITEM;
+ }
}
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 48825c8204a..f6ed2a1a320 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -44,12 +44,16 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
+import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
+import com.android.settings.fuelgauge.batteryusage.BatteryHistEntry;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.instrumentation.Instrumentable;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.LayoutPreference;
@@ -219,8 +223,11 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
return UserHandle.getUserId(batteryEntry.getUid());
}
- public static void startBatteryDetailPage(Activity caller,
- InstrumentedPreferenceFragment fragment, String packageName) {
+ /**
+ * Start packageName's battery detail page.
+ */
+ public static void startBatteryDetailPage(
+ Activity caller, Instrumentable instrumentable, String packageName) {
final Bundle args = new Bundle(3);
final PackageManager packageManager = caller.getPackageManager();
args.putString(EXTRA_PACKAGE_NAME, packageName);
@@ -235,7 +242,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
- .setSourceMetricsCategory(fragment.getMetricsCategory())
+ .setSourceMetricsCategory(instrumentable.getMetricsCategory())
.launch();
}
@@ -536,16 +543,13 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
return null;
}
if (totalTimeMs == 0) {
- final int batteryWithoutUsageTime = consumedPower > 0
- ? R.string.battery_usage_without_time : R.string.battery_not_usage_24hr;
- usageTimeSummary = getText(isChartGraphEnabled
- ? batteryWithoutUsageTime : R.string.battery_not_usage);
+ usageTimeSummary = getText(
+ isChartGraphEnabled && consumedPower > 0 ? R.string.battery_usage_without_time
+ : R.string.battery_not_usage);
} else if (slotTime == null) {
- // Shows summary text with past 24 hr or full charge if slot time is null.
- usageTimeSummary = isChartGraphEnabled
- ? getAppPast24HrActiveSummary(foregroundTimeMs, backgroundTimeMs, totalTimeMs)
- : getAppFullChargeActiveSummary(
- foregroundTimeMs, backgroundTimeMs, totalTimeMs);
+ // Shows summary text with last full charge if slot time is null.
+ usageTimeSummary = getAppFullChargeActiveSummary(
+ foregroundTimeMs, backgroundTimeMs, totalTimeMs);
} else {
// Shows summary text with slot time.
usageTimeSummary = getAppActiveSummaryWithSlotTime(
diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
index f5e21ddc1b2..9860240b289 100644
--- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
+++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
@@ -24,16 +24,13 @@ import android.app.backup.BackupHelper;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
-import android.content.pm.PackageManager;
-import android.content.pm.ParceledListSlice;
-import android.content.pm.UserInfo;
import android.os.Build;
import android.os.IDeviceIdleController;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
-import android.os.UserManager;
+import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -53,22 +50,13 @@ public final class BatteryBackupHelper implements BackupHelper {
private static final String DEVICE_IDLE_SERVICE = "deviceidle";
private static final boolean DEBUG = Build.TYPE.equals("userdebug");
- // Only the owner can see all apps.
- private static final int RETRIEVE_FLAG_ADMIN =
- PackageManager.MATCH_ANY_USER |
- PackageManager.MATCH_DISABLED_COMPONENTS |
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
- private static final int RETRIEVE_FLAG =
- PackageManager.MATCH_DISABLED_COMPONENTS |
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
-
static final String DELIMITER = ",";
static final String DELIMITER_MODE = ":";
static final String KEY_FULL_POWER_LIST = "full_power_list";
static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
@VisibleForTesting
- List mTestApplicationInfoList = null;
+ ArraySet mTestApplicationInfoList = null;
@VisibleForTesting
PowerAllowlistBackend mPowerAllowlistBackend;
@@ -146,7 +134,7 @@ public final class BatteryBackupHelper implements BackupHelper {
@VisibleForTesting
void backupOptimizationMode(BackupDataOutput data, List allowlistedApps) {
final long timestamp = System.currentTimeMillis();
- final List applications = getInstalledApplications();
+ final ArraySet applications = getInstalledApplications();
if (applications == null || applications.isEmpty()) {
Log.w(TAG, "no data found in the getInstalledApplications()");
return;
@@ -269,36 +257,11 @@ public final class BatteryBackupHelper implements BackupHelper {
|| powerAllowlistBackend.isDefaultActiveApp(packageName);
}
- private List getInstalledApplications() {
+ private ArraySet getInstalledApplications() {
if (mTestApplicationInfoList != null) {
return mTestApplicationInfoList;
}
- final List applications = new ArrayList<>();
- final UserManager um = mContext.getSystemService(UserManager.class);
- for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
- try {
- @SuppressWarnings("unchecked")
- final ParceledListSlice infoList =
- getIPackageManager().getInstalledApplications(
- userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
- userInfo.id);
- if (infoList != null) {
- applications.addAll(infoList.getList());
- }
- } catch (Exception e) {
- Log.e(TAG, "getInstalledApplications() is failed", e);
- return null;
- }
- }
- // Removes the application which is disabled by the system.
- for (int index = applications.size() - 1; index >= 0; index--) {
- final ApplicationInfo info = applications.get(index);
- if (info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
- && !info.enabled) {
- applications.remove(index);
- }
- }
- return applications;
+ return BatteryOptimizeUtils.getInstalledApplications(mContext, getIPackageManager());
}
private void debugLog(String debugContent) {
diff --git a/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java
deleted file mode 100644
index 02248c9af51..00000000000
--- a/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java
+++ /dev/null
@@ -1,726 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.settings.fuelgauge;
-
-import android.app.settings.SettingsEnums;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceGroup;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.R;
-import com.android.settings.SettingsActivity;
-import com.android.settings.core.InstrumentedPreferenceFragment;
-import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settings.overlay.FeatureFactory;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.core.lifecycle.LifecycleObserver;
-import com.android.settingslib.core.lifecycle.events.OnCreate;
-import com.android.settingslib.core.lifecycle.events.OnDestroy;
-import com.android.settingslib.core.lifecycle.events.OnResume;
-import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
-import com.android.settingslib.utils.StringUtil;
-import com.android.settingslib.widget.FooterPreference;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Controls the update for chart graph and the list items. */
-public class BatteryChartPreferenceController extends AbstractPreferenceController
- implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
- OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume,
- ExpandDividerPreference.OnExpandListener {
- private static final String TAG = "BatteryChartPreferenceController";
- private static final String KEY_FOOTER_PREF = "battery_graph_footer";
- private static final String PACKAGE_NAME_NONE = "none";
-
- /** Desired battery history size for timestamp slots. */
- public static final int DESIRED_HISTORY_SIZE = 25;
- private static final int CHART_LEVEL_ARRAY_SIZE = 13;
- private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE;
- private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2;
- private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3;
-
- // Keys for bundle instance to restore configurations.
- private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
- private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot";
-
- private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
-
- @VisibleForTesting
- Map> mBatteryIndexedMap;
-
- @VisibleForTesting Context mPrefContext;
- @VisibleForTesting BatteryUtils mBatteryUtils;
- @VisibleForTesting PreferenceGroup mAppListPrefGroup;
- @VisibleForTesting BatteryChartView mBatteryChartView;
- @VisibleForTesting ExpandDividerPreference mExpandDividerPreference;
-
- @VisibleForTesting boolean mIsExpanded = false;
- @VisibleForTesting int[] mBatteryHistoryLevels;
- @VisibleForTesting long[] mBatteryHistoryKeys;
- @VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
-
- private boolean mIs24HourFormat = false;
- private boolean mIsFooterPrefAdded = false;
- private PreferenceScreen mPreferenceScreen;
- private FooterPreference mFooterPreference;
-
- private final String mPreferenceKey;
- private final SettingsActivity mActivity;
- private final InstrumentedPreferenceFragment mFragment;
- private final CharSequence[] mNotAllowShowEntryPackages;
- private final CharSequence[] mNotAllowShowSummaryPackages;
- private final MetricsFeatureProvider mMetricsFeatureProvider;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- // Preference cache to avoid create new instance each time.
- @VisibleForTesting
- final Map mPreferenceCache = new HashMap<>();
- @VisibleForTesting
- final List mSystemEntries = new ArrayList<>();
-
- public BatteryChartPreferenceController(
- Context context, String preferenceKey,
- Lifecycle lifecycle, SettingsActivity activity,
- InstrumentedPreferenceFragment fragment) {
- super(context);
- mActivity = activity;
- mFragment = fragment;
- mPreferenceKey = preferenceKey;
- mIs24HourFormat = DateFormat.is24HourFormat(context);
- mMetricsFeatureProvider =
- FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
- mNotAllowShowEntryPackages =
- FeatureFactory.getFactory(context)
- .getPowerUsageFeatureProvider(context)
- .getHideApplicationEntries(context);
- mNotAllowShowSummaryPackages =
- FeatureFactory.getFactory(context)
- .getPowerUsageFeatureProvider(context)
- .getHideApplicationSummary(context);
- if (lifecycle != null) {
- lifecycle.addObserver(this);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (savedInstanceState == null) {
- return;
- }
- mTrapezoidIndex =
- savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
- mIsExpanded =
- savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
- Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b",
- mTrapezoidIndex, mIsExpanded));
- }
-
- @Override
- public void onResume() {
- final int currentUiMode =
- mContext.getResources().getConfiguration().uiMode
- & Configuration.UI_MODE_NIGHT_MASK;
- if (sUiMode != currentUiMode) {
- sUiMode = currentUiMode;
- BatteryDiffEntry.clearCache();
- Log.d(TAG, "clear icon and label cache since uiMode is changed");
- }
- mIs24HourFormat = DateFormat.is24HourFormat(mContext);
- mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
- }
-
- @Override
- public void onSaveInstanceState(Bundle savedInstance) {
- if (savedInstance == null) {
- return;
- }
- savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
- savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
- Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b",
- mTrapezoidIndex, mIsExpanded));
- }
-
- @Override
- public void onDestroy() {
- if (mActivity.isChangingConfigurations()) {
- BatteryDiffEntry.clearCache();
- }
- mHandler.removeCallbacksAndMessages(/*token=*/ null);
- mPreferenceCache.clear();
- if (mAppListPrefGroup != null) {
- mAppListPrefGroup.removeAll();
- }
- }
-
- @Override
- public void displayPreference(PreferenceScreen screen) {
- super.displayPreference(screen);
- mPreferenceScreen = screen;
- mPrefContext = screen.getContext();
- mAppListPrefGroup = screen.findPreference(mPreferenceKey);
- mAppListPrefGroup.setOrderingAsAdded(false);
- mAppListPrefGroup.setTitle(
- mPrefContext.getString(R.string.battery_app_usage_for_past_24));
- mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
- // Removes footer first until usage data is loaded to avoid flashing.
- if (mFooterPreference != null) {
- screen.removePreference(mFooterPreference);
- }
- }
-
- @Override
- public boolean isAvailable() {
- return true;
- }
-
- @Override
- public String getPreferenceKey() {
- return mPreferenceKey;
- }
-
- @Override
- public boolean handlePreferenceTreeClick(Preference preference) {
- if (!(preference instanceof PowerGaugePreference)) {
- return false;
- }
- final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
- final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
- final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
- final String packageName = histEntry.mPackageName;
- final boolean isAppEntry = histEntry.isAppEntry();
- mMetricsFeatureProvider.action(
- /* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
- /* action */ isAppEntry
- ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
- : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
- /* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
- TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
- (int) Math.round(diffEntry.getPercentOfTotal()));
- Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
- diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
- AdvancedPowerUsageDetail.startBatteryDetailPage(
- mActivity, mFragment, diffEntry, powerPref.getPercent(),
- isValidToShowSummary(packageName), getSlotInformation());
- return true;
- }
-
- @Override
- public void onSelect(int trapezoidIndex) {
- Log.d(TAG, "onChartSelect:" + trapezoidIndex);
- refreshUi(trapezoidIndex, /*isForce=*/ false);
- mMetricsFeatureProvider.action(
- mPrefContext,
- trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL
- ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
- : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
- }
-
- @Override
- public void onExpand(boolean isExpanded) {
- mIsExpanded = isExpanded;
- mMetricsFeatureProvider.action(
- mPrefContext,
- SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
- isExpanded);
- refreshExpandUi();
- }
-
- void setBatteryHistoryMap(
- final Map> batteryHistoryMap) {
- // Resets all battery history data relative variables.
- if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
- mBatteryIndexedMap = null;
- mBatteryHistoryKeys = null;
- mBatteryHistoryLevels = null;
- addFooterPreferenceIfNeeded(false);
- return;
- }
- mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
- mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE];
- for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
- final long timestamp = mBatteryHistoryKeys[index * 2];
- final Map entryMap = batteryHistoryMap.get(timestamp);
- if (entryMap == null || entryMap.isEmpty()) {
- Log.e(TAG, "abnormal entry list in the timestamp:"
- + ConvertUtils.utcToLocalTime(mPrefContext, timestamp));
- continue;
- }
- // Averages the battery level in each time slot to avoid corner conditions.
- float batteryLevelCounter = 0;
- for (BatteryHistEntry entry : entryMap.values()) {
- batteryLevelCounter += entry.mBatteryLevel;
- }
- mBatteryHistoryLevels[index] =
- Math.round(batteryLevelCounter / entryMap.size());
- }
- forceRefreshUi();
- Log.d(TAG, String.format(
- "setBatteryHistoryMap() size=%d key=%s\nlevels=%s",
- batteryHistoryMap.size(),
- ConvertUtils.utcToLocalTime(mPrefContext,
- mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
- Arrays.toString(mBatteryHistoryLevels)));
-
- // Loads item icon and label in the background.
- new LoadAllItemsInfoTask(batteryHistoryMap).execute();
- }
-
- void setBatteryChartView(final BatteryChartView batteryChartView) {
- if (mBatteryChartView != batteryChartView) {
- mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
- }
- }
-
- private void setBatteryChartViewInner(final BatteryChartView batteryChartView) {
- mBatteryChartView = batteryChartView;
- mBatteryChartView.setOnSelectListener(this);
- forceRefreshUi();
- }
-
- private void forceRefreshUi() {
- final int refreshIndex =
- mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID
- ? BatteryChartView.SELECTED_INDEX_ALL
- : mTrapezoidIndex;
- if (mBatteryChartView != null) {
- mBatteryChartView.setLevels(mBatteryHistoryLevels);
- mBatteryChartView.setSelectedIndex(refreshIndex);
- setTimestampLabel();
- }
- refreshUi(refreshIndex, /*isForce=*/ true);
- }
-
- @VisibleForTesting
- boolean refreshUi(int trapezoidIndex, boolean isForce) {
- // Invalid refresh condition.
- if (mBatteryIndexedMap == null
- || mBatteryChartView == null
- || (mTrapezoidIndex == trapezoidIndex && !isForce)) {
- return false;
- }
- Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
- trapezoidIndex, mBatteryIndexedMap.size(), isForce));
-
- mTrapezoidIndex = trapezoidIndex;
- mHandler.post(() -> {
- final long start = System.currentTimeMillis();
- removeAndCacheAllPrefs();
- addAllPreferences();
- refreshCategoryTitle();
- Log.d(TAG, String.format("refreshUi is finished in %d/ms",
- (System.currentTimeMillis() - start)));
- });
- return true;
- }
-
- private void addAllPreferences() {
- final List entries =
- mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
- addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
- if (entries == null) {
- Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
- return;
- }
- // Separates data into two groups and sort them individually.
- final List appEntries = new ArrayList<>();
- mSystemEntries.clear();
- entries.forEach(entry -> {
- final String packageName = entry.getPackageName();
- if (!isValidToShowEntry(packageName)) {
- Log.w(TAG, "ignore showing item:" + packageName);
- return;
- }
- if (entry.isSystemEntry()) {
- mSystemEntries.add(entry);
- } else {
- appEntries.add(entry);
- }
- // Validates the usage time if users click a specific slot.
- if (mTrapezoidIndex >= 0) {
- validateUsageTime(entry);
- }
- });
- Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR);
- Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
- Log.d(TAG, String.format("addAllPreferences() app=%d system=%d",
- appEntries.size(), mSystemEntries.size()));
-
- // Adds app entries to the list if it is not empty.
- if (!appEntries.isEmpty()) {
- addPreferenceToScreen(appEntries);
- }
- // Adds the expabable divider if we have system entries data.
- if (!mSystemEntries.isEmpty()) {
- if (mExpandDividerPreference == null) {
- mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
- mExpandDividerPreference.setOnExpandListener(this);
- mExpandDividerPreference.setIsExpanded(mIsExpanded);
- }
- mExpandDividerPreference.setOrder(
- mAppListPrefGroup.getPreferenceCount());
- mAppListPrefGroup.addPreference(mExpandDividerPreference);
- }
- refreshExpandUi();
- }
-
- @VisibleForTesting
- void addPreferenceToScreen(List entries) {
- if (mAppListPrefGroup == null || entries.isEmpty()) {
- return;
- }
- int prefIndex = mAppListPrefGroup.getPreferenceCount();
- for (BatteryDiffEntry entry : entries) {
- boolean isAdded = false;
- final String appLabel = entry.getAppLabel();
- final Drawable appIcon = entry.getAppIcon();
- if (TextUtils.isEmpty(appLabel) || appIcon == null) {
- Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
- continue;
- }
- final String prefKey = entry.mBatteryHistEntry.getKey();
- PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey);
- if (pref != null) {
- isAdded = true;
- Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
- } else {
- pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
- }
- // Creates new innstance if cached preference is not found.
- if (pref == null) {
- pref = new PowerGaugePreference(mPrefContext);
- pref.setKey(prefKey);
- mPreferenceCache.put(prefKey, pref);
- }
- pref.setIcon(appIcon);
- pref.setTitle(appLabel);
- pref.setOrder(prefIndex);
- pref.setPercent(entry.getPercentOfTotal());
- pref.setSingleLineTitle(true);
- // Sets the BatteryDiffEntry to preference for launching detailed page.
- pref.setBatteryDiffEntry(entry);
- pref.setEnabled(entry.validForRestriction());
- setPreferenceSummary(pref, entry);
- if (!isAdded) {
- mAppListPrefGroup.addPreference(pref);
- }
- prefIndex++;
- }
- }
-
- private void removeAndCacheAllPrefs() {
- if (mAppListPrefGroup == null
- || mAppListPrefGroup.getPreferenceCount() == 0) {
- return;
- }
- final int prefsCount = mAppListPrefGroup.getPreferenceCount();
- for (int index = 0; index < prefsCount; index++) {
- final Preference pref = mAppListPrefGroup.getPreference(index);
- if (TextUtils.isEmpty(pref.getKey())) {
- continue;
- }
- mPreferenceCache.put(pref.getKey(), pref);
- }
- mAppListPrefGroup.removeAll();
- }
-
- private void refreshExpandUi() {
- if (mIsExpanded) {
- addPreferenceToScreen(mSystemEntries);
- } else {
- // Removes and recycles all system entries to hide all of them.
- for (BatteryDiffEntry entry : mSystemEntries) {
- final String prefKey = entry.mBatteryHistEntry.getKey();
- final Preference pref = mAppListPrefGroup.findPreference(prefKey);
- if (pref != null) {
- mAppListPrefGroup.removePreference(pref);
- mPreferenceCache.put(pref.getKey(), pref);
- }
- }
- }
- }
-
- @VisibleForTesting
- void refreshCategoryTitle() {
- final String slotInformation = getSlotInformation();
- Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation));
- if (mAppListPrefGroup != null) {
- mAppListPrefGroup.setTitle(
- getSlotInformation(/*isApp=*/ true, slotInformation));
- }
- if (mExpandDividerPreference != null) {
- mExpandDividerPreference.setTitle(
- getSlotInformation(/*isApp=*/ false, slotInformation));
- }
- }
-
- private String getSlotInformation(boolean isApp, String slotInformation) {
- // Null means we show all information without a specific time slot.
- if (slotInformation == null) {
- return isApp
- ? mPrefContext.getString(R.string.battery_app_usage_for_past_24)
- : mPrefContext.getString(R.string.battery_system_usage_for_past_24);
- } else {
- return isApp
- ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
- : mPrefContext.getString(R.string.battery_system_usage_for ,slotInformation);
- }
- }
-
- private String getSlotInformation() {
- if (mTrapezoidIndex < 0) {
- return null;
- }
- final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
- mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat);
- final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
- mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat);
- return mIs24HourFormat
- ? String.format("%s–%s", fromHour, toHour)
- : String.format("%s – %s", fromHour, toHour);
- }
-
- @VisibleForTesting
- void setPreferenceSummary(
- PowerGaugePreference preference, BatteryDiffEntry entry) {
- final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
- final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
- final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
- // Checks whether the package is allowed to show summary or not.
- if (!isValidToShowSummary(entry.getPackageName())) {
- preference.setSummary(null);
- return;
- }
- String usageTimeSummary = null;
- // Not shows summary for some system components without usage time.
- if (totalUsageTimeInMs == 0) {
- preference.setSummary(null);
- // Shows background summary only if we don't have foreground usage time.
- } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
- usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
- // Shows total usage summary only if total usage time is small.
- } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
- usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
- } else {
- usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
- // Shows background usage time if it is larger than a minute.
- if (backgroundUsageTimeInMs > 0) {
- usageTimeSummary +=
- "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
- }
- }
- preference.setSummary(usageTimeSummary);
- }
-
- private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
- if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
- return mPrefContext.getString(
- isBackground
- ? R.string.battery_usage_background_less_than_one_minute
- : R.string.battery_usage_total_less_than_one_minute);
- }
- final CharSequence timeSequence =
- StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs,
- /*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
- final int resourceId =
- isBackground
- ? R.string.battery_usage_for_background_time
- : R.string.battery_usage_for_total_time;
- return mPrefContext.getString(resourceId, timeSequence);
- }
-
- @VisibleForTesting
- boolean isValidToShowSummary(String packageName) {
- return !contains(packageName, mNotAllowShowSummaryPackages);
- }
-
- @VisibleForTesting
- boolean isValidToShowEntry(String packageName) {
- return !contains(packageName, mNotAllowShowEntryPackages);
- }
-
- @VisibleForTesting
- void setTimestampLabel() {
- if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
- return;
- }
- final long latestTimestamp =
- mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
- mBatteryChartView.setLatestTimestamp(latestTimestamp);
- }
-
- private void addFooterPreferenceIfNeeded(boolean containAppItems) {
- if (mIsFooterPrefAdded || mFooterPreference == null) {
- return;
- }
- mIsFooterPrefAdded = true;
- mFooterPreference.setTitle(mPrefContext.getString(
- containAppItems
- ? R.string.battery_usage_screen_footer
- : R.string.battery_usage_screen_footer_empty));
- mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
- }
-
- private static boolean contains(String target, CharSequence[] packageNames) {
- if (target != null && packageNames != null) {
- for (CharSequence packageName : packageNames) {
- if (TextUtils.equals(target, packageName)) {
- return true;
- }
- }
- }
- return false;
- }
-
- @VisibleForTesting
- static boolean validateUsageTime(BatteryDiffEntry entry) {
- final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
- final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
- final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
- if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
- || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
- || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) {
- Log.e(TAG, "validateUsageTime() fail for\n" + entry);
- return false;
- }
- return true;
- }
-
- /** Used for {@link AppBatteryPreferenceController}. */
- public static List getBatteryLast24HrUsageData(Context context) {
- final long start = System.currentTimeMillis();
- final Map> batteryHistoryMap =
- FeatureFactory.getFactory(context)
- .getPowerUsageFeatureProvider(context)
- .getBatteryHistory(context);
- if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
- return null;
- }
- Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms",
- batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
- final Map> batteryIndexedMap =
- ConvertUtils.getIndexedUsageMap(
- context,
- /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
- getBatteryHistoryKeys(batteryHistoryMap),
- batteryHistoryMap,
- /*purgeLowPercentageAndFakeData=*/ true);
- return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL);
- }
-
- /** Used for {@link AppBatteryPreferenceController}. */
- public static BatteryDiffEntry getBatteryLast24HrUsageData(
- Context context, String packageName, int userId) {
- if (packageName == null) {
- return null;
- }
- final List entries = getBatteryLast24HrUsageData(context);
- if (entries == null) {
- return null;
- }
- for (BatteryDiffEntry entry : entries) {
- final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
- if (batteryHistEntry != null
- && batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
- && batteryHistEntry.mUserId == userId
- && packageName.equals(entry.getPackageName())) {
- return entry;
- }
- }
- return null;
- }
-
- private static long[] getBatteryHistoryKeys(
- final Map> batteryHistoryMap) {
- final List batteryHistoryKeyList =
- new ArrayList<>(batteryHistoryMap.keySet());
- Collections.sort(batteryHistoryKeyList);
- final long[] batteryHistoryKeys = new long[CHART_KEY_ARRAY_SIZE];
- for (int index = 0; index < CHART_KEY_ARRAY_SIZE; index++) {
- batteryHistoryKeys[index] = batteryHistoryKeyList.get(index);
- }
- return batteryHistoryKeys;
- }
-
- // Loads all items icon and label in the background.
- private final class LoadAllItemsInfoTask
- extends AsyncTask>> {
-
- private long[] mBatteryHistoryKeysCache;
- private Map> mBatteryHistoryMap;
-
- private LoadAllItemsInfoTask(
- Map> batteryHistoryMap) {
- this.mBatteryHistoryMap = batteryHistoryMap;
- this.mBatteryHistoryKeysCache = mBatteryHistoryKeys;
- }
-
- @Override
- protected Map> doInBackground(Void... voids) {
- if (mPrefContext == null || mBatteryHistoryKeysCache == null) {
- return null;
- }
- final long startTime = System.currentTimeMillis();
- final Map> indexedUsageMap =
- ConvertUtils.getIndexedUsageMap(
- mPrefContext, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
- mBatteryHistoryKeysCache, mBatteryHistoryMap,
- /*purgeLowPercentageAndFakeData=*/ true);
- // Pre-loads each BatteryDiffEntry relative icon and label for all slots.
- for (List entries : indexedUsageMap.values()) {
- entries.forEach(entry -> entry.loadLabelAndIcon());
- }
- Log.d(TAG, String.format("execute LoadAllItemsInfoTask in %d/ms",
- (System.currentTimeMillis() - startTime)));
- return indexedUsageMap;
- }
-
- @Override
- protected void onPostExecute(
- Map> indexedUsageMap) {
- mBatteryHistoryMap = null;
- mBatteryHistoryKeysCache = null;
- if (indexedUsageMap == null) {
- return;
- }
- // Posts results back to main thread to refresh UI.
- mHandler.post(() -> {
- mBatteryIndexedMap = indexedUsageMap;
- forceRefreshUi();
- });
- }
- }
-}
diff --git a/src/com/android/settings/fuelgauge/BatteryChartView.java b/src/com/android/settings/fuelgauge/BatteryChartView.java
deleted file mode 100644
index ff61fcead5a..00000000000
--- a/src/com/android/settings/fuelgauge/BatteryChartView.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-package com.android.settings.fuelgauge;
-
-import static java.lang.Math.round;
-
-import static com.android.settings.Utils.formatPercentage;
-
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.CornerPathEffect;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.TextView;
-
-import androidx.appcompat.widget.AppCompatImageView;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.settings.R;
-import com.android.settings.overlay.FeatureFactory;
-import com.android.settingslib.Utils;
-
-import java.time.Clock;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-/** A widget component to draw chart graph. */
-public class BatteryChartView extends AppCompatImageView implements View.OnClickListener,
- AccessibilityManager.AccessibilityStateChangeListener {
- private static final String TAG = "BatteryChartView";
- private static final List ACCESSIBILITY_SERVICE_NAMES =
- Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");
-
- private static final int DEFAULT_TRAPEZOID_COUNT = 12;
- private static final int DEFAULT_TIMESTAMP_COUNT = 4;
- private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
- private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
- private static final long UPDATE_STATE_DELAYED_TIME = 500L;
-
- /** Selects all trapezoid shapes. */
- public static final int SELECTED_INDEX_ALL = -1;
- public static final int SELECTED_INDEX_INVALID = -2;
-
- /** A callback listener for selected group index is updated. */
- public interface OnSelectListener {
- void onSelect(int trapezoidIndex);
- }
-
- private int mDividerWidth;
- private int mDividerHeight;
- private int mTrapezoidCount;
- private float mTrapezoidVOffset;
- private float mTrapezoidHOffset;
- private boolean mIsSlotsClickabled;
- private String[] mPercentages = getPercentages();
-
- @VisibleForTesting int mHoveredIndex = SELECTED_INDEX_INVALID;
- @VisibleForTesting int mSelectedIndex = SELECTED_INDEX_INVALID;
- @VisibleForTesting String[] mTimestamps;
-
- // Colors for drawing the trapezoid shape and dividers.
- private int mTrapezoidColor;
- private int mTrapezoidSolidColor;
- private int mTrapezoidHoverColor;
- // For drawing the percentage information.
- private int mTextPadding;
- private final Rect mIndent = new Rect();
- private final Rect[] mPercentageBounds =
- new Rect[] {new Rect(), new Rect(), new Rect()};
- // For drawing the timestamp information.
- private final Rect[] mTimestampsBounds =
- new Rect[] {new Rect(), new Rect(), new Rect(), new Rect()};
-
- @VisibleForTesting
- Handler mHandler = new Handler();
- @VisibleForTesting
- final Runnable mUpdateClickableStateRun = () -> updateClickableState();
-
- private int[] mLevels;
- private Paint mTextPaint;
- private Paint mDividerPaint;
- private Paint mTrapezoidPaint;
-
- @VisibleForTesting
- Paint mTrapezoidCurvePaint = null;
- private TrapezoidSlot[] mTrapezoidSlots;
- // Records the location to calculate selected index.
- private float mTouchUpEventX = Float.MIN_VALUE;
- private BatteryChartView.OnSelectListener mOnSelectListener;
-
- public BatteryChartView(Context context) {
- super(context, null);
- }
-
- public BatteryChartView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initializeColors(context);
- // Registers the click event listener.
- setOnClickListener(this);
- setSelectedIndex(SELECTED_INDEX_ALL);
- setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
- setClickable(false);
- setLatestTimestamp(0);
- }
-
- /** Sets the total trapezoid count for drawing. */
- public void setTrapezoidCount(int trapezoidCount) {
- Log.i(TAG, "trapezoidCount:" + trapezoidCount);
- mTrapezoidCount = trapezoidCount;
- mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
- // Allocates the trapezoid slot array.
- for (int index = 0; index < trapezoidCount; index++) {
- mTrapezoidSlots[index] = new TrapezoidSlot();
- }
- invalidate();
- }
-
- /** Sets all levels value to draw the trapezoid shape */
- public void setLevels(int[] levels) {
- Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
- if (levels == null) {
- mLevels = null;
- return;
- }
- // We should provide trapezoid count + 1 data to draw all trapezoids.
- mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
- setClickable(false);
- invalidate();
- if (mLevels == null) {
- return;
- }
- // Sets the chart is clickable if there is at least one valid item in it.
- for (int index = 0; index < mLevels.length - 1; index++) {
- if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
- setClickable(true);
- break;
- }
- }
- }
-
- /** Sets the selected group index to draw highlight effect. */
- public void setSelectedIndex(int index) {
- if (mSelectedIndex != index) {
- mSelectedIndex = index;
- invalidate();
- // Callbacks to the listener if we have.
- if (mOnSelectListener != null) {
- mOnSelectListener.onSelect(mSelectedIndex);
- }
- }
- }
-
- /** Sets the callback to monitor the selected group index. */
- public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
- mOnSelectListener = listener;
- }
-
- /** Sets the companion {@link TextView} for percentage information. */
- public void setCompanionTextView(TextView textView) {
- if (textView != null) {
- // Pre-draws the view first to load style atttributions into paint.
- textView.draw(new Canvas());
- mTextPaint = textView.getPaint();
- } else {
- mTextPaint = null;
- }
- setVisibility(View.VISIBLE);
- requestLayout();
- }
-
- /** Sets the latest timestamp for drawing into x-axis information. */
- public void setLatestTimestamp(long latestTimestamp) {
- if (latestTimestamp == 0) {
- latestTimestamp = Clock.systemUTC().millis();
- }
- if (mTimestamps == null) {
- mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
- }
- final long timeSlotOffset =
- DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
- final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
- for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
- mTimestamps[index] =
- ConvertUtils.utcToLocalTimeHour(
- getContext(),
- latestTimestamp - (TIMESTAMP_GAPS_COUNT - index) * timeSlotOffset,
- is24HourFormat);
- }
- requestLayout();
- }
-
- @Override
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // Measures text bounds and updates indent configuration.
- if (mTextPaint != null) {
- for (int index = 0; index < mPercentages.length; index++) {
- mTextPaint.getTextBounds(
- mPercentages[index], 0, mPercentages[index].length(),
- mPercentageBounds[index]);
- }
- // Updates the indent configurations.
- mIndent.top = mPercentageBounds[0].height();
- mIndent.right = mPercentageBounds[0].width() + mTextPadding;
-
- if (mTimestamps != null) {
- int maxHeight = 0;
- for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
- mTextPaint.getTextBounds(
- mTimestamps[index], 0, mTimestamps[index].length(),
- mTimestampsBounds[index]);
- maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
- }
- mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
- }
- Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
- } else {
- mIndent.set(0, 0, 0, 0);
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
- drawHorizontalDividers(canvas);
- drawVerticalDividers(canvas);
- drawTrapezoids(canvas);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // Caches the location to calculate selected trapezoid index.
- final int action = event.getAction();
- switch (action) {
- case MotionEvent.ACTION_UP:
- mTouchUpEventX = event.getX();
- break;
- case MotionEvent.ACTION_CANCEL:
- mTouchUpEventX = Float.MIN_VALUE; // reset
- break;
- }
- return super.onTouchEvent(event);
- }
-
- @Override
- public boolean onHoverEvent(MotionEvent event) {
- final int action = event.getAction();
- switch (action) {
- case MotionEvent.ACTION_HOVER_ENTER:
- case MotionEvent.ACTION_HOVER_MOVE:
- final int trapezoidIndex = getTrapezoidIndex(event.getX());
- if (mHoveredIndex != trapezoidIndex) {
- mHoveredIndex = trapezoidIndex;
- invalidate();
- }
- break;
- }
- return super.onHoverEvent(event);
- }
-
- @Override
- public void onHoverChanged(boolean hovered) {
- super.onHoverChanged(hovered);
- if (!hovered) {
- mHoveredIndex = SELECTED_INDEX_INVALID; // reset
- invalidate();
- }
- }
-
- @Override
- public void onClick(View view) {
- if (mTouchUpEventX == Float.MIN_VALUE) {
- Log.w(TAG, "invalid motion event for onClick() callback");
- return;
- }
- final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
- // Ignores the click event if the level is zero.
- if (trapezoidIndex == SELECTED_INDEX_INVALID
- || !isValidToDraw(trapezoidIndex)) {
- return;
- }
- // Selects all if users click the same trapezoid item two times.
- if (trapezoidIndex == mSelectedIndex) {
- setSelectedIndex(SELECTED_INDEX_ALL);
- } else {
- setSelectedIndex(trapezoidIndex);
- }
- view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- updateClickableState();
- mContext.getSystemService(AccessibilityManager.class)
- .addAccessibilityStateChangeListener(/*listener=*/ this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mContext.getSystemService(AccessibilityManager.class)
- .removeAccessibilityStateChangeListener(/*listener=*/ this);
- mHandler.removeCallbacks(mUpdateClickableStateRun);
- }
-
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- Log.d(TAG, "onAccessibilityStateChanged:" + enabled);
- mHandler.removeCallbacks(mUpdateClickableStateRun);
- // We should delay it a while since accessibility manager will spend
- // some times to bind with new enabled accessibility services.
- mHandler.postDelayed(
- mUpdateClickableStateRun, UPDATE_STATE_DELAYED_TIME);
- }
-
- private void updateClickableState() {
- final Context context = mContext;
- mIsSlotsClickabled =
- FeatureFactory.getFactory(context)
- .getPowerUsageFeatureProvider(context)
- .isChartGraphSlotsEnabled(context)
- && !isAccessibilityEnabled(context);
- Log.d(TAG, "isChartGraphSlotsEnabled:" + mIsSlotsClickabled);
- setClickable(isClickable());
- // Initializes the trapezoid curve paint for non-clickable case.
- if (!mIsSlotsClickabled && mTrapezoidCurvePaint == null) {
- mTrapezoidCurvePaint = new Paint();
- mTrapezoidCurvePaint.setAntiAlias(true);
- mTrapezoidCurvePaint.setColor(mTrapezoidSolidColor);
- mTrapezoidCurvePaint.setStyle(Paint.Style.STROKE);
- mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
- } else if (mIsSlotsClickabled) {
- mTrapezoidCurvePaint = null;
- // Sets levels again to force update the click state.
- setLevels(mLevels);
- }
- invalidate();
- }
-
- @Override
- public void setClickable(boolean clickable) {
- super.setClickable(mIsSlotsClickabled && clickable);
- }
-
- @VisibleForTesting
- void setClickableForce(boolean clickable) {
- super.setClickable(clickable);
- }
-
- private void initializeColors(Context context) {
- setBackgroundColor(Color.TRANSPARENT);
- mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
- mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
- mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
- com.android.internal.R.attr.colorAccentSecondaryVariant);
- // Initializes the divider line paint.
- final Resources resources = getContext().getResources();
- mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
- mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
- mDividerPaint = new Paint();
- mDividerPaint.setAntiAlias(true);
- mDividerPaint.setColor(DIVIDER_COLOR);
- mDividerPaint.setStyle(Paint.Style.STROKE);
- mDividerPaint.setStrokeWidth(mDividerWidth);
- Log.i(TAG, "mDividerWidth:" + mDividerWidth);
- Log.i(TAG, "mDividerHeight:" + mDividerHeight);
- // Initializes the trapezoid paint.
- mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
- mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
- mTrapezoidPaint = new Paint();
- mTrapezoidPaint.setAntiAlias(true);
- mTrapezoidPaint.setColor(mTrapezoidSolidColor);
- mTrapezoidPaint.setStyle(Paint.Style.FILL);
- mTrapezoidPaint.setPathEffect(
- new CornerPathEffect(
- resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
- // Initializes for drawing text information.
- mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
- }
-
- private void drawHorizontalDividers(Canvas canvas) {
- final int width = getWidth() - mIndent.right;
- final int height = getHeight() - mIndent.top - mIndent.bottom;
- // Draws the top divider line for 100% curve.
- float offsetY = mIndent.top + mDividerWidth * .5f;
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 0, offsetY);
-
- // Draws the center divider line for 50% curve.
- final float availableSpace =
- height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
- offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 1, offsetY);
-
- // Draws the bottom divider line for 0% curve.
- offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
- canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
- drawPercentage(canvas, /*index=*/ 2, offsetY);
- }
-
- private void drawPercentage(Canvas canvas, int index, float offsetY) {
- if (mTextPaint != null) {
- canvas.drawText(
- mPercentages[index],
- getWidth() - mPercentageBounds[index].width() - mPercentageBounds[index].left,
- offsetY + mPercentageBounds[index].height() *.5f,
- mTextPaint);
- }
- }
-
- private void drawVerticalDividers(Canvas canvas) {
- final int width = getWidth() - mIndent.right;
- final int dividerCount = mTrapezoidCount + 1;
- final float dividerSpace = dividerCount * mDividerWidth;
- final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
- final float bottomY = getHeight() - mIndent.bottom;
- final float startY = bottomY - mDividerHeight;
- final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
- // Draws each vertical dividers.
- float startX = mDividerWidth * .5f;
- for (int index = 0; index < dividerCount; index++) {
- canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
- final float nextX = startX + mDividerWidth + unitWidth;
- // Updates the trapezoid slots for drawing.
- if (index < mTrapezoidSlots.length) {
- mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
- mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
- }
- startX = nextX;
- }
- // Draws the timestamp slot information.
- if (mTimestamps != null) {
- final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
- final float baselineX = mDividerWidth * .5f;
- final float offsetX = mDividerWidth + unitWidth;
- final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
- for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
- xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
- }
- drawTimestamp(canvas, xOffsets);
- }
- }
-
- private void drawTimestamp(Canvas canvas, float[] xOffsets) {
- // Draws the 1st timestamp info.
- canvas.drawText(
- mTimestamps[0],
- xOffsets[0] - mTimestampsBounds[0].left,
- getTimestampY(0), mTextPaint);
- final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
- // Draws the last timestamp info.
- canvas.drawText(
- mTimestamps[latestIndex],
- xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
- - mTimestampsBounds[latestIndex].left,
- getTimestampY(latestIndex), mTextPaint);
- // Draws the rest of timestamp info since it is located in the center.
- for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
- canvas.drawText(
- mTimestamps[index],
- xOffsets[index] -
- (mTimestampsBounds[index].width() - mTimestampsBounds[index].left) * .5f,
- getTimestampY(index), mTextPaint);
-
- }
- }
-
- private int getTimestampY(int index) {
- return getHeight() - mTimestampsBounds[index].height()
- + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
- + round(mTextPadding * 1.5f);
- }
-
- private void drawTrapezoids(Canvas canvas) {
- // Ignores invalid trapezoid data.
- if (mLevels == null) {
- return;
- }
- final float trapezoidBottom =
- getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
- - mTrapezoidVOffset;
- final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
- final float unitHeight = availableSpace / 100f;
- // Draws all trapezoid shapes into the canvas.
- final Path trapezoidPath = new Path();
- Path trapezoidCurvePath = null;
- for (int index = 0; index < mTrapezoidCount; index++) {
- // Not draws the trapezoid for corner or not initialization cases.
- if (!isValidToDraw(index)) {
- if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
- canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
- trapezoidCurvePath = null;
- }
- continue;
- }
- // Configures the trapezoid paint color.
- final int trapezoidColor =
- !mIsSlotsClickabled
- ? mTrapezoidColor
- : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
- ? mTrapezoidSolidColor : mTrapezoidColor;
- final boolean isHoverState =
- mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
- mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
-
- final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
- final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
- trapezoidPath.reset();
- trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
- trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
- trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
- trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
- // A tricky way to make the trapezoid shape drawing the rounded corner.
- trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
- trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
- // Draws the trapezoid shape into canvas.
- canvas.drawPath(trapezoidPath, mTrapezoidPaint);
-
- // Generates path for non-clickable trapezoid curve.
- if (mTrapezoidCurvePaint != null) {
- if (trapezoidCurvePath == null) {
- trapezoidCurvePath= new Path();
- trapezoidCurvePath.moveTo(mTrapezoidSlots[index].mLeft, leftTop);
- } else {
- trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
- }
- trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
- }
- }
- // Draws the trapezoid curve for non-clickable case.
- if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
- canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
- trapezoidCurvePath = null;
- }
- }
-
- // Searches the corresponding trapezoid index from x location.
- private int getTrapezoidIndex(float x) {
- for (int index = 0; index < mTrapezoidSlots.length; index++) {
- final TrapezoidSlot slot = mTrapezoidSlots[index];
- if (x >= slot.mLeft - mTrapezoidHOffset
- && x <= slot.mRight + mTrapezoidHOffset) {
- return index;
- }
- }
- return SELECTED_INDEX_INVALID;
- }
-
- private boolean isValidToDraw(int trapezoidIndex) {
- return mLevels != null
- && trapezoidIndex >= 0
- && trapezoidIndex < mLevels.length - 1
- && mLevels[trapezoidIndex] != 0
- && mLevels[trapezoidIndex + 1] != 0;
- }
-
- private static String[] getPercentages() {
- return new String[] {
- formatPercentage(/*percentage=*/ 100, /*round=*/ true),
- formatPercentage(/*percentage=*/ 50, /*round=*/ true),
- formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
- }
-
- @VisibleForTesting
- static boolean isAccessibilityEnabled(Context context) {
- final AccessibilityManager accessibilityManager =
- context.getSystemService(AccessibilityManager.class);
- if (!accessibilityManager.isEnabled()) {
- return false;
- }
- final List serviceInfoList =
- accessibilityManager.getEnabledAccessibilityServiceList(
- AccessibilityServiceInfo.FEEDBACK_SPOKEN
- | AccessibilityServiceInfo.FEEDBACK_GENERIC);
- for (AccessibilityServiceInfo info : serviceInfoList) {
- for (String serviceName : ACCESSIBILITY_SERVICE_NAMES) {
- final String serviceId = info.getId();
- if (serviceId != null && serviceId.contains(serviceName)) {
- Log.d(TAG, "acccessibilityEnabled:" + serviceId);
- return true;
- }
- }
- }
- return false;
- }
-
- // A container class for each trapezoid left and right location.
- private static final class TrapezoidSlot {
- public float mLeft;
- public float mRight;
-
- @Override
- public String toString() {
- return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
- }
- }
-}
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 9306a34df67..dbfacc6907c 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -19,7 +19,15 @@ package com.android.settings.fuelgauge;
import android.annotation.IntDef;
import android.app.AppOpsManager;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -43,6 +51,15 @@ public class BatteryOptimizeUtils {
private final String mPackageName;
private final int mUid;
+ // If current user is admin, match apps from all users. Otherwise, only match the currect user.
+ private static final int RETRIEVE_FLAG_ADMIN =
+ PackageManager.MATCH_ANY_USER
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+ private static final int RETRIEVE_FLAG =
+ PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+
// Optimization modes.
static final int MODE_UNKNOWN = 0;
static final int MODE_RESTRICTED = 1;
@@ -90,28 +107,20 @@ public class BatteryOptimizeUtils {
return getAppOptimizationMode(mMode, mAllowListed);
}
+ /** Resets optimization mode for all applications. */
+ public static void resetAppOptimizationMode(
+ Context context, IPackageManager ipm, AppOpsManager aom) {
+ resetAppOptimizationMode(context, ipm, aom,
+ PowerAllowlistBackend.getInstance(context), BatteryUtils.getInstance(context));
+ }
+
/** Sets the {@link OptimizationMode} for associated app. */
public void setAppUsageState(@OptimizationMode int mode) {
if (getAppOptimizationMode(mMode, mAllowListed) == mode) {
Log.w(TAG, "set the same optimization mode for: " + mPackageName);
return;
}
-
- AsyncTask.execute(() -> {
- switch (mode) {
- case MODE_RESTRICTED:
- setAppOptimizationMode(AppOpsManager.MODE_IGNORED, /* allowListed */ false);
- break;
- case MODE_UNRESTRICTED:
- setAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ true);
- break;
- case MODE_OPTIMIZED:
- setAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false);
- break;
- default:
- Log.d(TAG, "set unknown app optimization mode.");
- }
- });
+ setAppUsageStateInternal(mode, mUid, mPackageName, mBatteryUtils, mPowerAllowListBackend);
}
/**
@@ -126,25 +135,111 @@ public class BatteryOptimizeUtils {
*/
public boolean isSystemOrDefaultApp() {
mPowerAllowListBackend.refreshList();
+ return isSystemOrDefaultApp(mPowerAllowListBackend, mPackageName);
+ }
- return mPowerAllowListBackend.isSysAllowlisted(mPackageName)
- || mPowerAllowListBackend.isDefaultActiveApp(mPackageName);
+ /**
+ * Gets the list of installed applications.
+ */
+ public static ArraySet getInstalledApplications(
+ Context context, IPackageManager ipm) {
+ final ArraySet applications = new ArraySet<>();
+ final UserManager um = context.getSystemService(UserManager.class);
+ for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
+ try {
+ @SuppressWarnings("unchecked")
+ final ParceledListSlice infoList = ipm.getInstalledApplications(
+ userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
+ userInfo.id);
+ if (infoList != null) {
+ applications.addAll(infoList.getList());
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "getInstalledApplications() is failed", e);
+ return null;
+ }
+ }
+ // Removes the application which is disabled by the system.
+ applications.removeIf(
+ info -> info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
+ && !info.enabled);
+ return applications;
+ }
+
+ @VisibleForTesting
+ static void resetAppOptimizationMode(
+ Context context, IPackageManager ipm, AppOpsManager aom,
+ PowerAllowlistBackend allowlistBackend, BatteryUtils batteryUtils) {
+ final ArraySet applications = getInstalledApplications(context, ipm);
+ if (applications == null || applications.isEmpty()) {
+ Log.w(TAG, "no data found in the getInstalledApplications()");
+ return;
+ }
+
+ allowlistBackend.refreshList();
+ // Resets optimization mode for each application.
+ for (ApplicationInfo info : applications) {
+ final int mode = aom.checkOpNoThrow(
+ AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
+ @OptimizationMode
+ final int optimizationMode = getAppOptimizationMode(
+ mode, allowlistBackend.isAllowlisted(info.packageName));
+ // Ignores default optimized/unknown state or system/default apps.
+ if (optimizationMode == MODE_OPTIMIZED
+ || optimizationMode == MODE_UNKNOWN
+ || isSystemOrDefaultApp(allowlistBackend, info.packageName)) {
+ continue;
+ }
+
+ // Resets to the default mode: MODE_OPTIMIZED.
+ setAppUsageStateInternal(MODE_OPTIMIZED, info.uid, info.packageName, batteryUtils,
+ allowlistBackend);
+ }
}
String getPackageName() {
return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
}
- private void setAppOptimizationMode(int appStandbyMode, boolean allowListed) {
+ private static boolean isSystemOrDefaultApp(
+ PowerAllowlistBackend powerAllowlistBackend, String packageName) {
+ return powerAllowlistBackend.isSysAllowlisted(packageName)
+ || powerAllowlistBackend.isDefaultActiveApp(packageName);
+ }
+
+ private static void setAppUsageStateInternal(
+ @OptimizationMode int mode, int uid, String packageName, BatteryUtils batteryUtils,
+ PowerAllowlistBackend powerAllowlistBackend) {
+ if (mode == MODE_UNKNOWN) {
+ Log.d(TAG, "set unknown app optimization mode.");
+ return;
+ }
+
+ // MODE_RESTRICTED = AppOpsManager.MODE_IGNORED + !allowListed
+ // MODE_UNRESTRICTED = AppOpsManager.MODE_ALLOWED + allowListed
+ // MODE_OPTIMIZED = AppOpsManager.MODE_ALLOWED + !allowListed
+ final int appOpsManagerMode =
+ mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
+ final boolean allowListed = mode == MODE_UNRESTRICTED;
+
+ AsyncTask.execute(() -> {
+ setAppOptimizationModeInternal(appOpsManagerMode, allowListed, uid, packageName,
+ batteryUtils, powerAllowlistBackend);
+ });
+ }
+
+ private static void setAppOptimizationModeInternal(
+ int appStandbyMode, boolean allowListed, int uid, String packageName,
+ BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend) {
try {
- mBatteryUtils.setForceAppStandby(mUid, mPackageName, appStandbyMode);
+ batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
if (allowListed) {
- mPowerAllowListBackend.addApp(mPackageName);
+ powerAllowlistBackend.addApp(packageName);
} else {
- mPowerAllowListBackend.removeApp(mPackageName);
+ powerAllowlistBackend.removeApp(packageName);
}
} catch (Exception e) {
- Log.e(TAG, "set OPTIMIZED failed for " + mPackageName, e);
+ Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
}
}
@@ -154,8 +249,6 @@ public class BatteryOptimizeUtils {
mMode = mAppOpsManager
.checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(TAG, String.format("refresh %s state, allowlisted = %s, mode = %d",
- mPackageName,
- mAllowListed,
- mMode));
+ mPackageName, mAllowListed, mMode));
}
}
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index 29c2fc96d86..a6c48a48d6b 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -69,6 +69,8 @@ public class BatteryUtils {
public static final int UID_REMOVED_APPS = -4;
/** Special UID value for data usage by tethering. */
public static final int UID_TETHERING = -5;
+ /** Special UID for aggregated other users. */
+ public static final long UID_OTHER_USERS = Long.MIN_VALUE;
@Retention(RetentionPolicy.SOURCE)
@IntDef({StatusType.SCREEN_USAGE,
@@ -190,7 +192,7 @@ public class BatteryUtils {
* Returns true if the specified battery consumer should be excluded from
* battery consumption lists, either short or full.
*/
- boolean shouldHideUidBatteryConsumerUnconditionally(UidBatteryConsumer consumer,
+ public boolean shouldHideUidBatteryConsumerUnconditionally(UidBatteryConsumer consumer,
String[] packages) {
final int uid = consumer.getUid();
return uid == UID_TETHERING
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index ef201e440f7..94a93b83be0 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import android.content.Intent;
import android.net.Uri;
import android.util.SparseIntArray;
+import com.android.settings.fuelgauge.batteryusage.BatteryHistEntry;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.Map;
@@ -73,6 +74,7 @@ public interface PowerUsageFeatureProvider {
/**
* Returns an improved projection curve for future battery level.
+ *
* @param zeroTime timestamps (array keys) are shifted by this amount
*/
SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime);
@@ -84,21 +86,20 @@ public interface PowerUsageFeatureProvider {
/**
* Checks whether debugging should be enabled for battery estimates.
- * @return
*/
boolean isEstimateDebugEnabled();
/**
* Converts the provided string containing the remaining time into a debug string for enhanced
* estimates.
- * @param timeRemaining
+ *
* @return A string containing the estimate and a label indicating it is an enhanced estimate
*/
String getEnhancedEstimateDebugString(String timeRemaining);
/**
* Converts the provided string containing the remaining time into a debug string.
- * @param timeRemaining
+ *
* @return A string containing the estimate and a label indicating it is a normal estimate
*/
String getOldEstimateDebugString(String timeRemaining);
@@ -114,7 +115,7 @@ public interface PowerUsageFeatureProvider {
* to their next charging time.
*
* @param id Optional string used to identify the caller for metrics. Usually the class name of
- * the caller
+ * the caller
*/
boolean getEarlyWarningSignal(Context context, String id);
@@ -138,6 +139,11 @@ public interface PowerUsageFeatureProvider {
*/
boolean isAdaptiveChargingSupported();
+ /**
+ * Returns {@code true} if current defender mode is extra defend
+ */
+ boolean isExtraDefend();
+
/**
* Gets a intent for one time bypass charge limited to resume charging.
*/
@@ -148,6 +154,11 @@ public interface PowerUsageFeatureProvider {
*/
Map> getBatteryHistory(Context context);
+ /**
+ * Returns battery history data since last full charge with corresponding timestamp key.
+ */
+ Map> getBatteryHistorySinceLastFullCharge(Context context);
+
/**
* Returns {@link Uri} to monitor battery history data is update.
*/
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index bc25381d04d..0adfc9d1537 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,11 +26,13 @@ import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
+import com.android.settings.fuelgauge.batteryusage.BatteryHistEntry;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.Map;
import java.util.Set;
+/** Implementation of {@code PowerUsageFeatureProvider} */
public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider {
private static final String PACKAGE_CALENDAR_PROVIDER = "com.android.providers.calendar";
@@ -158,11 +160,22 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
return null;
}
+ @Override
+ public boolean isExtraDefend() {
+ return false;
+ }
+
@Override
public Map> getBatteryHistory(Context context) {
return null;
}
+ @Override
+ public Map> getBatteryHistorySinceLastFullCharge(
+ Context context) {
+ return null;
+ }
+
@Override
public Uri getBatteryHistoryUri() {
return null;
diff --git a/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java b/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java
index b960d4c4e06..b85db40d954 100644
--- a/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java
@@ -46,12 +46,15 @@ public class RestrictAppPreferenceController extends BasePreferenceController {
private AppOpsManager mAppOpsManager;
private InstrumentedPreferenceFragment mPreferenceFragment;
private UserManager mUserManager;
+ private boolean mEnableAppBatteryUsagePage;
public RestrictAppPreferenceController(Context context) {
super(context, KEY_RESTRICT_APP);
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mUserManager = context.getSystemService(UserManager.class);
mAppInfos = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager, mUserManager);
+ mEnableAppBatteryUsagePage =
+ mContext.getResources().getBoolean(R.bool.config_app_battery_usage_list_enabled);
}
public RestrictAppPreferenceController(InstrumentedPreferenceFragment preferenceFragment) {
@@ -61,7 +64,8 @@ public class RestrictAppPreferenceController extends BasePreferenceController {
@Override
public int getAvailabilityStatus() {
- return mAppInfos.size() > 0 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ return mAppInfos.size() > 0 && !mEnableAppBatteryUsagePage ? AVAILABLE
+ : CONDITIONALLY_UNAVAILABLE;
}
@Override
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java b/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java
index d920a8ef777..d508603c772 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java
@@ -36,6 +36,7 @@ public class BatteryManagerPreferenceController extends BasePreferenceController
private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
private AppOpsManager mAppOpsManager;
private UserManager mUserManager;
+ private boolean mEnableAppBatteryUsagePage;
public BatteryManagerPreferenceController(Context context) {
super(context, KEY_BATTERY_MANAGER);
@@ -43,6 +44,8 @@ public class BatteryManagerPreferenceController extends BasePreferenceController
context).getPowerUsageFeatureProvider(context);
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mUserManager = context.getSystemService(UserManager.class);
+ mEnableAppBatteryUsagePage =
+ mContext.getResources().getBoolean(R.bool.config_app_battery_usage_list_enabled);
}
@Override
@@ -53,9 +56,12 @@ public class BatteryManagerPreferenceController extends BasePreferenceController
@Override
public void updateState(Preference preference) {
super.updateState(preference);
- final int num = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager, mUserManager).size();
+ if (!mEnableAppBatteryUsagePage) {
+ final int num = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager,
+ mUserManager).size();
- updateSummary(preference, num);
+ updateSummary(preference, num);
+ }
}
@VisibleForTesting
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
index 4b9858753da..95145ba8216 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
@@ -72,7 +72,8 @@ public class BatteryTipLoader extends AsyncLoaderCompat> {
tips.add(new SmartBatteryDetector(
context, policy, batteryInfo, context.getContentResolver()).detect());
tips.add(new EarlyWarningDetector(policy, context).detect());
- tips.add(new BatteryDefenderDetector(batteryInfo).detect());
+ tips.add(new BatteryDefenderDetector(
+ batteryInfo, context.getApplicationContext()).detect());
Collections.sort(tips);
return tips;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java
index d12784f4ce3..4dcdc0cc111 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java
@@ -29,7 +29,6 @@ import androidx.annotation.NonNull;
import com.android.internal.util.CollectionUtils;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
-import com.android.settings.fuelgauge.batterytip.actions.BatteryDefenderAction;
import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction;
import com.android.settings.fuelgauge.batterytip.actions.OpenBatterySaverAction;
import com.android.settings.fuelgauge.batterytip.actions.OpenRestrictAppFragmentAction;
@@ -107,8 +106,6 @@ public class BatteryTipUtils {
}
case BatteryTip.TipType.REMOVE_APP_RESTRICTION:
return new UnrestrictAppAction(settingsActivity, (UnrestrictAppTip) batteryTip);
- case BatteryTip.TipType.BATTERY_DEFENDER:
- return new BatteryDefenderAction(settingsActivity);
default:
return null;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
index 5befa330b0a..87d4a0b44a5 100644
--- a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
@@ -16,26 +16,33 @@
package com.android.settings.fuelgauge.batterytip.detectors;
+import android.content.Context;
+
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.batterytip.tips.BatteryDefenderTip;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+import com.android.settings.overlay.FeatureFactory;
/**
* Detect whether the battery is overheated
*/
public class BatteryDefenderDetector implements BatteryTipDetector {
- private BatteryInfo mBatteryInfo;
+ private final BatteryInfo mBatteryInfo;
+ private final Context mContext;
- public BatteryDefenderDetector(BatteryInfo batteryInfo) {
+ public BatteryDefenderDetector(BatteryInfo batteryInfo, Context context) {
mBatteryInfo = batteryInfo;
+ mContext = context;
}
@Override
public BatteryTip detect() {
- final int state =
- mBatteryInfo.isOverheated
- ? BatteryTip.StateType.NEW
- : BatteryTip.StateType.INVISIBLE;
- return new BatteryDefenderTip(state);
+ if (mBatteryInfo.isOverheated) {
+ final boolean extraDefend = FeatureFactory.getFactory(mContext)
+ .getPowerUsageFeatureProvider(mContext)
+ .isExtraDefend();
+ return new BatteryDefenderTip(BatteryTip.StateType.NEW, extraDefend);
+ }
+ return new BatteryDefenderTip(BatteryTip.StateType.INVISIBLE);
}
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
index a2890ad9b40..2fb56500834 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
@@ -18,18 +18,38 @@ package com.android.settings.fuelgauge.batterytip.tips;
import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
import android.os.Parcel;
+import android.util.Log;
+
+import androidx.preference.Preference;
import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.widget.CardPreference;
+import com.android.settingslib.HelpUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import java.text.NumberFormat;
+
/**
* Tip to show current battery is overheated
*/
public class BatteryDefenderTip extends BatteryTip {
+ private static final String TAG = "BatteryDefenderTip";
+ private boolean mExtraDefend = false;
+
public BatteryDefenderTip(@StateType int state) {
+ this(state, false);
+ }
+
+ public BatteryDefenderTip(@StateType int state, boolean extraDefend) {
super(TipType.BATTERY_DEFENDER, state, true /* showDialog */);
+ mExtraDefend = extraDefend;
+ mShowDialog = false;
}
private BatteryDefenderTip(Parcel in) {
@@ -43,6 +63,14 @@ public class BatteryDefenderTip extends BatteryTip {
@Override
public CharSequence getSummary(Context context) {
+ if (mExtraDefend) {
+ final int extraValue = context.getResources()
+ .getInteger(R.integer.config_battery_extra_tip_value);
+ final String extraPercentage = NumberFormat.getPercentInstance()
+ .format(extraValue * 0.01f);
+ return context.getString(
+ R.string.battery_tip_limited_temporarily_extra_summary, extraPercentage);
+ }
return context.getString(R.string.battery_tip_limited_temporarily_summary);
}
@@ -62,6 +90,63 @@ public class BatteryDefenderTip extends BatteryTip {
mState);
}
+ @Override
+ public void updatePreference(Preference preference) {
+ super.updatePreference(preference);
+ final Context context = preference.getContext();
+
+ CardPreference cardPreference = castToCardPreferenceSafely(preference);
+ if (cardPreference == null) {
+ Log.e(TAG, "cast Preference to CardPreference failed");
+ return;
+ }
+
+ cardPreference.setSelectable(false);
+ cardPreference.setPrimaryButtonText(
+ context.getString(R.string.battery_tip_charge_to_full_button));
+ cardPreference.setPrimaryButtonClickListener(
+ unused -> {
+ resumeCharging(context);
+ preference.setVisible(false);
+ });
+ cardPreference.setPrimaryButtonVisible(isPluggedIn(context));
+
+ cardPreference.setSecondaryButtonText(context.getString(R.string.learn_more));
+ cardPreference.setSecondaryButtonClickListener(
+ button -> button.startActivityForResult(
+ HelpUtils.getHelpIntent(
+ context,
+ context.getString(R.string.help_url_battery_defender),
+ /* backupContext */ ""), /* requestCode */ 0));
+ cardPreference.setSecondaryButtonVisible(true);
+ cardPreference.setSecondaryButtonContentDescription(context.getString(
+ R.string.battery_tip_limited_temporarily_sec_button_content_description));
+ }
+
+ private CardPreference castToCardPreferenceSafely(Preference preference) {
+ return preference instanceof CardPreference ? (CardPreference) preference : null;
+ }
+
+ private void resumeCharging(Context context) {
+ final Intent intent =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getResumeChargeIntent();
+ if (intent != null) {
+ context.sendBroadcast(intent);
+ }
+
+ Log.i(TAG, "send resume charging broadcast intent=" + intent);
+ }
+
+ private boolean isPluggedIn(Context context) {
+ final Intent batteryIntent =
+ context.registerReceiver(
+ /* receiver= */ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ return batteryIntent != null
+ && batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+ }
+
public static final Creator CREATOR = new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new BatteryDefenderTip(in);
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java
index a0e470f5c41..3c560f8f18e 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java
@@ -21,12 +21,13 @@ import android.os.Parcel;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/**
* Tip to suggest user to remove app restriction. This is the empty tip and it is only used in
- * {@link com.android.settings.fuelgauge.AdvancedPowerUsageDetail} to create dialog.
+ * {@link AdvancedPowerUsageDetail} to create dialog.
*/
public class UnrestrictAppTip extends BatteryTip {
private AppInfo mAppInfo;
diff --git a/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryAppListPreferenceController.java
similarity index 97%
rename from src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryAppListPreferenceController.java
index 9731ab7c708..f752dad5f89 100644
--- a/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryAppListPreferenceController.java
@@ -1,5 +1,6 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -11,11 +12,9 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- *
- *
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.app.Activity;
import android.content.Context;
@@ -35,8 +34,8 @@ import android.os.UserManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
-import android.util.SparseArray;
import android.util.Log;
+import android.util.SparseArray;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -48,6 +47,8 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
+import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -72,6 +73,7 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro
private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 20;
private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver";
+ private static final String NOT_AVAILABLE = "not_available";
@VisibleForTesting
PreferenceGroup mAppListGroup;
@@ -230,7 +232,7 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro
if (sConfig.shouldShowBatteryAttributionList(mContext)) {
final int dischargePercentage = getDischargePercentage(batteryUsageStats);
final List usageList =
- getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ true);
+ getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ true);
final double totalPower = batteryUsageStats.getConsumedPower();
final int numSippers = usageList.size();
for (int i = 0; i < numSippers; i++) {
@@ -292,7 +294,7 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro
}
final int dischargePercentage = getDischargePercentage(batteryUsageStats);
final List usageList =
- getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ false);
+ getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ false);
final double totalPower = batteryUsageStats.getConsumedPower();
for (int i = 0; i < usageList.size(); i++) {
final BatteryEntry entry = usageList.get(i);
@@ -458,8 +460,8 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro
private void cacheRemoveAllPrefs(PreferenceGroup group) {
mPreferenceCache = new ArrayMap<>();
- final int N = group.getPreferenceCount();
- for (int i = 0; i < N; i++) {
+ final int n = group.getPreferenceCount();
+ for (int i = 0; i < n; i++) {
Preference p = group.getPreference(i);
if (TextUtils.isEmpty(p.getKey())) {
continue;
@@ -563,7 +565,6 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro
}
private void addNotAvailableMessage() {
- final String NOT_AVAILABLE = "not_available";
Preference notAvailable = getCachedPreference(NOT_AVAILABLE);
if (notAvailable == null) {
notAvailable = new Preference(mPrefContext);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
new file mode 100644
index 00000000000..2c924885c3b
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -0,0 +1,850 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.core.InstrumentedPreferenceFragment;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnCreate;
+import com.android.settingslib.core.lifecycle.events.OnDestroy;
+import com.android.settingslib.core.lifecycle.events.OnResume;
+import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
+import com.android.settingslib.utils.StringUtil;
+import com.android.settingslib.widget.FooterPreference;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Controls the update for chart graph and the list items. */
+public class BatteryChartPreferenceController extends AbstractPreferenceController
+ implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
+ OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener {
+ private static final String TAG = "BatteryChartPreferenceController";
+ private static final String KEY_FOOTER_PREF = "battery_graph_footer";
+ private static final String PACKAGE_NAME_NONE = "none";
+ private static final int ENABLED_ICON_ALPHA = 255;
+ private static final int DISABLED_ICON_ALPHA = 255 / 3;
+
+ private static final long FADE_IN_ANIMATION_DURATION = 400L;
+ private static final long FADE_OUT_ANIMATION_DURATION = 200L;
+
+ // Keys for bundle instance to restore configurations.
+ private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
+ private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
+ private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
+
+ private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
+
+ @VisibleForTesting
+ Map> mBatteryUsageMap;
+
+ @VisibleForTesting
+ Context mPrefContext;
+ @VisibleForTesting
+ BatteryUtils mBatteryUtils;
+ @VisibleForTesting
+ PreferenceGroup mAppListPrefGroup;
+ @VisibleForTesting
+ ExpandDividerPreference mExpandDividerPreference;
+ @VisibleForTesting
+ boolean mIsExpanded = false;
+
+ @VisibleForTesting
+ BatteryChartView mDailyChartView;
+ @VisibleForTesting
+ BatteryChartView mHourlyChartView;
+
+ @VisibleForTesting
+ int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+ @VisibleForTesting
+ int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+
+ private boolean mIs24HourFormat;
+ private boolean mIsFooterPrefAdded = false;
+ private View mBatteryChartViewGroup;
+ private View mCategoryTitleView;
+ private PreferenceScreen mPreferenceScreen;
+ private FooterPreference mFooterPreference;
+ private TextView mChartSummaryTextView;
+ private BatteryChartViewModel mDailyViewModel;
+ private List mHourlyViewModels;
+
+ private final String mPreferenceKey;
+ private final SettingsActivity mActivity;
+ private final InstrumentedPreferenceFragment mFragment;
+ private final CharSequence[] mNotAllowShowSummaryPackages;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final AnimatorListenerAdapter mHourlyChartFadeInAdapter =
+ createHourlyChartAnimatorListenerAdapter(/*visible=*/ true);
+ private final AnimatorListenerAdapter mHourlyChartFadeOutAdapter =
+ createHourlyChartAnimatorListenerAdapter(/*visible=*/ false);
+
+ @VisibleForTesting
+ final DailyChartLabelTextGenerator mDailyChartLabelTextGenerator =
+ new DailyChartLabelTextGenerator();
+ @VisibleForTesting
+ final HourlyChartLabelTextGenerator mHourlyChartLabelTextGenerator =
+ new HourlyChartLabelTextGenerator();
+
+ // Preference cache to avoid create new instance each time.
+ @VisibleForTesting
+ final Map mPreferenceCache = new HashMap<>();
+
+ public BatteryChartPreferenceController(
+ Context context, String preferenceKey,
+ Lifecycle lifecycle, SettingsActivity activity,
+ InstrumentedPreferenceFragment fragment) {
+ super(context);
+ mActivity = activity;
+ mFragment = fragment;
+ mPreferenceKey = preferenceKey;
+ mIs24HourFormat = DateFormat.is24HourFormat(context);
+ mMetricsFeatureProvider =
+ FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
+ mNotAllowShowSummaryPackages =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getHideApplicationSummary(context);
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ mDailyChartIndex =
+ savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
+ mHourlyChartIndex =
+ savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
+ mIsExpanded =
+ savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
+ Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+ mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
+ }
+
+ @Override
+ public void onResume() {
+ final int currentUiMode =
+ mContext.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK;
+ if (sUiMode != currentUiMode) {
+ sUiMode = currentUiMode;
+ BatteryDiffEntry.clearCache();
+ Log.d(TAG, "clear icon and label cache since uiMode is changed");
+ }
+ mIs24HourFormat = DateFormat.is24HourFormat(mContext);
+ mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstance) {
+ if (savedInstance == null) {
+ return;
+ }
+ savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
+ savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
+ savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
+ Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+ mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mActivity.isChangingConfigurations()) {
+ BatteryDiffEntry.clearCache();
+ }
+ mHandler.removeCallbacksAndMessages(/*token=*/ null);
+ mPreferenceCache.clear();
+ if (mAppListPrefGroup != null) {
+ mAppListPrefGroup.removeAll();
+ }
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreferenceScreen = screen;
+ mPrefContext = screen.getContext();
+ mAppListPrefGroup = screen.findPreference(mPreferenceKey);
+ mAppListPrefGroup.setOrderingAsAdded(false);
+ mAppListPrefGroup.setTitle("");
+ mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
+ // Removes footer first until usage data is loaded to avoid flashing.
+ if (mFooterPreference != null) {
+ screen.removePreference(mFooterPreference);
+ }
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return mPreferenceKey;
+ }
+
+ @Override
+ public boolean handlePreferenceTreeClick(Preference preference) {
+ if (!(preference instanceof PowerGaugePreference)) {
+ return false;
+ }
+ final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
+ final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
+ final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
+ final String packageName = histEntry.mPackageName;
+ final boolean isAppEntry = histEntry.isAppEntry();
+ mMetricsFeatureProvider.action(
+ /* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
+ /* action */ isAppEntry
+ ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
+ : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
+ /* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
+ TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
+ (int) Math.round(diffEntry.getPercentOfTotal()));
+ Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
+ diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
+ AdvancedPowerUsageDetail.startBatteryDetailPage(
+ mActivity, mFragment, diffEntry, powerPref.getPercent(),
+ isValidToShowSummary(packageName), getSlotInformation());
+ return true;
+ }
+
+ @Override
+ public void onExpand(boolean isExpanded) {
+ mIsExpanded = isExpanded;
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
+ isExpanded);
+ refreshExpandUi();
+ }
+
+ void setBatteryHistoryMap(
+ final Map> batteryHistoryMap) {
+ Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
+ : ("size=" + batteryHistoryMap.size())));
+ // Ensure the battery chart group is visible for users.
+ animateBatteryChartViewGroup();
+ final BatteryLevelData batteryLevelData =
+ DataProcessor.getBatteryLevelData(mContext, mHandler, batteryHistoryMap,
+ batteryUsageMap -> {
+ mBatteryUsageMap = batteryUsageMap;
+ refreshUi();
+ });
+ Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ SettingsEnums.ACTION_BATTERY_HISTORY_LOADED,
+ getTotalHours(batteryLevelData));
+
+ if (batteryLevelData == null) {
+ mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+ mDailyViewModel = null;
+ mHourlyViewModels = null;
+ refreshUi();
+ return;
+ }
+ mDailyViewModel = new BatteryChartViewModel(
+ batteryLevelData.getDailyBatteryLevels().getLevels(),
+ batteryLevelData.getDailyBatteryLevels().getTimestamps(),
+ BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS,
+ mDailyChartLabelTextGenerator);
+ mHourlyViewModels = new ArrayList<>();
+ for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay :
+ batteryLevelData.getHourlyBatteryLevelsPerDay()) {
+ mHourlyViewModels.add(new BatteryChartViewModel(
+ hourlyBatteryLevelsPerDay.getLevels(),
+ hourlyBatteryLevelsPerDay.getTimestamps(),
+ BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS,
+ mHourlyChartLabelTextGenerator));
+ }
+ refreshUi();
+ }
+
+ void setBatteryChartView(@NonNull final BatteryChartView dailyChartView,
+ @NonNull final BatteryChartView hourlyChartView) {
+ final View parentView = (View) dailyChartView.getParent();
+ if (parentView != null && parentView.getId() == R.id.battery_chart_group) {
+ mBatteryChartViewGroup = (View) dailyChartView.getParent();
+ }
+ if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) {
+ mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
+ animateBatteryChartViewGroup();
+ }
+ if (mBatteryChartViewGroup != null) {
+ final View grandparentView = (View) mBatteryChartViewGroup.getParent();
+ mChartSummaryTextView = grandparentView != null
+ ? grandparentView.findViewById(R.id.chart_summary) : null;
+ }
+ }
+
+ private void setBatteryChartViewInner(@NonNull final BatteryChartView dailyChartView,
+ @NonNull final BatteryChartView hourlyChartView) {
+ mDailyChartView = dailyChartView;
+ mDailyChartView.setOnSelectListener(trapezoidIndex -> {
+ if (mDailyChartIndex == trapezoidIndex) {
+ return;
+ }
+ Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
+ mDailyChartIndex = trapezoidIndex;
+ mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+ refreshUi();
+ requestAccessibilityFocusForCategoryTitle(mDailyChartView);
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
+ ? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
+ : SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
+ mDailyChartIndex);
+ });
+ mHourlyChartView = hourlyChartView;
+ mHourlyChartView.setOnSelectListener(trapezoidIndex -> {
+ if (mHourlyChartIndex == trapezoidIndex) {
+ return;
+ }
+ Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex);
+ mHourlyChartIndex = trapezoidIndex;
+ refreshUi();
+ requestAccessibilityFocusForCategoryTitle(mHourlyChartView);
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
+ ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
+ : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
+ mHourlyChartIndex);
+ });
+ refreshUi();
+ }
+
+ @VisibleForTesting
+ boolean refreshUi() {
+ if (mDailyChartView == null || mHourlyChartView == null) {
+ // Chart views are not initialized.
+ return false;
+ }
+
+ // When mDailyViewModel or mHourlyViewModels is null, there is no battery level data.
+ // This is mainly in 2 cases:
+ // 1) battery data is within 2 hours
+ // 2) no battery data in the latest 7 days (power off >= 7 days)
+ final boolean refreshUiResult = mDailyViewModel == null || mHourlyViewModels == null
+ ? refreshUiWithNoLevelDataCase()
+ : refreshUiWithLevelDataCase();
+
+ if (!refreshUiResult) {
+ return false;
+ }
+
+ mHandler.post(() -> {
+ final long start = System.currentTimeMillis();
+ removeAndCacheAllPrefs();
+ addAllPreferences();
+ refreshCategoryTitle();
+ Log.d(TAG, String.format("refreshUi is finished in %d/ms",
+ (System.currentTimeMillis() - start)));
+ });
+ return true;
+ }
+
+ private boolean refreshUiWithNoLevelDataCase() {
+ setChartSummaryVisible(false);
+ if (mBatteryUsageMap == null) {
+ // There is no battery level data and battery usage data is not ready, wait for data
+ // ready to refresh UI. Show nothing temporarily.
+ mDailyChartView.setVisibility(View.GONE);
+ mHourlyChartView.setVisibility(View.GONE);
+ mDailyChartView.setViewModel(null);
+ mHourlyChartView.setViewModel(null);
+ return false;
+ } else if (mBatteryUsageMap
+ .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
+ .get(BatteryChartViewModel.SELECTED_INDEX_ALL) == null) {
+ // There is no battery level data and battery usage data, show an empty hourly chart
+ // view.
+ mDailyChartView.setVisibility(View.GONE);
+ mHourlyChartView.setVisibility(View.VISIBLE);
+ mHourlyChartView.setViewModel(null);
+ removeAndCacheAllPrefs();
+ addFooterPreferenceIfNeeded(false);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean refreshUiWithLevelDataCase() {
+ setChartSummaryVisible(true);
+ // Gets valid battery level data.
+ if (isBatteryLevelDataInOneDay()) {
+ // Only 1 day data, hide the daily chart view.
+ mDailyChartView.setVisibility(View.GONE);
+ mDailyChartIndex = 0;
+ } else {
+ mDailyChartView.setVisibility(View.VISIBLE);
+ mDailyViewModel.setSelectedIndex(mDailyChartIndex);
+ mDailyChartView.setViewModel(mDailyViewModel);
+ }
+
+ if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
+ // Multiple days are selected, hide the hourly chart view.
+ animateBatteryHourlyChartView(/*visible=*/ false);
+ } else {
+ animateBatteryHourlyChartView(/*visible=*/ true);
+ final BatteryChartViewModel hourlyViewModel =
+ mHourlyViewModels.get(mDailyChartIndex);
+ hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
+ mHourlyChartView.setViewModel(hourlyViewModel);
+ }
+
+ if (mBatteryUsageMap == null) {
+ // Battery usage data is not ready, wait for data ready to refresh UI.
+ return false;
+ }
+ return true;
+ }
+
+ private void addAllPreferences() {
+ final BatteryDiffData batteryDiffData =
+ mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
+ addFooterPreferenceIfNeeded(batteryDiffData != null
+ && (!batteryDiffData.getAppDiffEntryList().isEmpty()
+ || !batteryDiffData.getSystemDiffEntryList().isEmpty()));
+ if (batteryDiffData == null) {
+ Log.w(TAG, "cannot find BatteryDiffEntry for daily_index: " + mDailyChartIndex
+ + " hourly_index: " + mHourlyChartIndex);
+ return;
+ }
+ // Adds app entries to the list if it is not empty.
+ if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
+ addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
+ }
+ // Adds the expandable divider if we have system entries data.
+ if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
+ if (mExpandDividerPreference == null) {
+ mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
+ mExpandDividerPreference.setOnExpandListener(this);
+ mExpandDividerPreference.setIsExpanded(mIsExpanded);
+ }
+ mExpandDividerPreference.setOrder(
+ mAppListPrefGroup.getPreferenceCount());
+ mAppListPrefGroup.addPreference(mExpandDividerPreference);
+ }
+ refreshExpandUi();
+ }
+
+ @VisibleForTesting
+ void addPreferenceToScreen(List entries) {
+ if (mAppListPrefGroup == null || entries.isEmpty()) {
+ return;
+ }
+ int prefIndex = mAppListPrefGroup.getPreferenceCount();
+ for (BatteryDiffEntry entry : entries) {
+ boolean isAdded = false;
+ final String appLabel = entry.getAppLabel();
+ final Drawable appIcon = entry.getAppIcon();
+ if (TextUtils.isEmpty(appLabel) || appIcon == null) {
+ Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
+ continue;
+ }
+ final String prefKey = entry.mBatteryHistEntry.getKey();
+ PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey);
+ if (pref != null) {
+ isAdded = true;
+ Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
+ } else {
+ pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
+ }
+ // Creates new innstance if cached preference is not found.
+ if (pref == null) {
+ pref = new PowerGaugePreference(mPrefContext);
+ pref.setKey(prefKey);
+ mPreferenceCache.put(prefKey, pref);
+ }
+ pref.setIcon(appIcon);
+ pref.setTitle(appLabel);
+ pref.setOrder(prefIndex);
+ pref.setPercent(entry.getPercentOfTotal());
+ pref.setSingleLineTitle(true);
+ // Sets the BatteryDiffEntry to preference for launching detailed page.
+ pref.setBatteryDiffEntry(entry);
+ pref.setEnabled(entry.validForRestriction());
+ setPreferenceSummary(pref, entry);
+ if (!isAdded) {
+ mAppListPrefGroup.addPreference(pref);
+ }
+ appIcon.setAlpha(pref.isEnabled() ? ENABLED_ICON_ALPHA : DISABLED_ICON_ALPHA);
+ prefIndex++;
+ }
+ }
+
+ private void removeAndCacheAllPrefs() {
+ if (mAppListPrefGroup == null
+ || mAppListPrefGroup.getPreferenceCount() == 0) {
+ return;
+ }
+ final int prefsCount = mAppListPrefGroup.getPreferenceCount();
+ for (int index = 0; index < prefsCount; index++) {
+ final Preference pref = mAppListPrefGroup.getPreference(index);
+ if (TextUtils.isEmpty(pref.getKey())) {
+ continue;
+ }
+ mPreferenceCache.put(pref.getKey(), pref);
+ }
+ mAppListPrefGroup.removeAll();
+ }
+
+ private void refreshExpandUi() {
+ final List systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get(
+ mHourlyChartIndex).getSystemDiffEntryList();
+ if (mIsExpanded) {
+ addPreferenceToScreen(systemEntries);
+ } else {
+ // Removes and recycles all system entries to hide all of them.
+ for (BatteryDiffEntry entry : systemEntries) {
+ final String prefKey = entry.mBatteryHistEntry.getKey();
+ final Preference pref = mAppListPrefGroup.findPreference(prefKey);
+ if (pref != null) {
+ mAppListPrefGroup.removePreference(pref);
+ mPreferenceCache.put(pref.getKey(), pref);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void refreshCategoryTitle() {
+ final String slotInformation = getSlotInformation();
+ Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation));
+ if (mAppListPrefGroup != null) {
+ mAppListPrefGroup.setTitle(
+ getSlotInformation(/*isApp=*/ true, slotInformation));
+ }
+ if (mExpandDividerPreference != null) {
+ mExpandDividerPreference.setTitle(
+ getSlotInformation(/*isApp=*/ false, slotInformation));
+ }
+ }
+
+ private void requestAccessibilityFocusForCategoryTitle(View view) {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return;
+ }
+ if (mCategoryTitleView == null) {
+ mCategoryTitleView = view.getRootView().findViewById(com.android.internal.R.id.title);
+ }
+ if (mCategoryTitleView != null) {
+ mCategoryTitleView.requestAccessibilityFocus();
+ }
+ }
+
+ private String getSlotInformation(boolean isApp, String slotInformation) {
+ // TODO: Updates the right slot information from daily and hourly chart selection.
+ // Null means we show all information without a specific time slot.
+ if (slotInformation == null) {
+ return isApp
+ ? mPrefContext.getString(R.string.battery_app_usage)
+ : mPrefContext.getString(R.string.battery_system_usage);
+ } else {
+ return isApp
+ ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
+ : mPrefContext.getString(R.string.battery_system_usage_for, slotInformation);
+ }
+ }
+
+ @VisibleForTesting
+ String getSlotInformation() {
+ if (mDailyViewModel == null || mHourlyViewModels == null) {
+ // No data
+ return null;
+ }
+ if (isAllSelected()) {
+ return null;
+ }
+
+ final String selectedDayText = mDailyViewModel.getFullText(mDailyChartIndex);
+ if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
+ return selectedDayText;
+ }
+
+ final String selectedHourText = mHourlyViewModels.get(mDailyChartIndex).getFullText(
+ mHourlyChartIndex);
+ if (isBatteryLevelDataInOneDay()) {
+ return selectedHourText;
+ }
+
+ return String.format("%s %s", selectedDayText, selectedHourText);
+ }
+
+ @VisibleForTesting
+ void setPreferenceSummary(
+ PowerGaugePreference preference, BatteryDiffEntry entry) {
+ final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
+ final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
+ final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ // Checks whether the package is allowed to show summary or not.
+ if (!isValidToShowSummary(entry.getPackageName())) {
+ preference.setSummary(null);
+ return;
+ }
+ String usageTimeSummary = null;
+ // Not shows summary for some system components without usage time.
+ if (totalUsageTimeInMs == 0) {
+ preference.setSummary(null);
+ // Shows background summary only if we don't have foreground usage time.
+ } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
+ usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
+ // Shows total usage summary only if total usage time is small.
+ } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
+ usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
+ } else {
+ usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
+ // Shows background usage time if it is larger than a minute.
+ if (backgroundUsageTimeInMs > 0) {
+ usageTimeSummary +=
+ "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
+ }
+ }
+ preference.setSummary(usageTimeSummary);
+ }
+
+ private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
+ if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
+ return mPrefContext.getString(
+ isBackground
+ ? R.string.battery_usage_background_less_than_one_minute
+ : R.string.battery_usage_total_less_than_one_minute);
+ }
+ final CharSequence timeSequence =
+ StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs,
+ /*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
+ final int resourceId =
+ isBackground
+ ? R.string.battery_usage_for_background_time
+ : R.string.battery_usage_for_total_time;
+ return mPrefContext.getString(resourceId, timeSequence);
+ }
+
+ @VisibleForTesting
+ boolean isValidToShowSummary(String packageName) {
+ return !DataProcessor.contains(packageName, mNotAllowShowSummaryPackages);
+ }
+
+ private void animateBatteryChartViewGroup() {
+ if (mBatteryChartViewGroup != null && mBatteryChartViewGroup.getAlpha() == 0) {
+ mBatteryChartViewGroup.animate().alpha(1f).setDuration(FADE_IN_ANIMATION_DURATION)
+ .start();
+ }
+ }
+
+ private void animateBatteryHourlyChartView(final boolean visible) {
+ if (mHourlyChartView == null) {
+ return;
+ }
+
+ if (visible) {
+ mHourlyChartView.setVisibility(View.VISIBLE);
+ mHourlyChartView.animate()
+ .alpha(1f)
+ .setDuration(FADE_IN_ANIMATION_DURATION)
+ .setListener(mHourlyChartFadeInAdapter)
+ .start();
+ } else {
+ mHourlyChartView.animate()
+ .alpha(0f)
+ .setDuration(FADE_OUT_ANIMATION_DURATION)
+ .setListener(mHourlyChartFadeOutAdapter)
+ .start();
+ }
+ }
+
+ private void setChartSummaryVisible(final boolean visible) {
+ if (mChartSummaryTextView != null) {
+ mChartSummaryTextView.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private AnimatorListenerAdapter createHourlyChartAnimatorListenerAdapter(
+ final boolean visible) {
+ final int visibility = visible ? View.VISIBLE : View.GONE;
+
+ return new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (mHourlyChartView != null) {
+ mHourlyChartView.setVisibility(visibility);
+ }
+ }
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ if (mHourlyChartView != null) {
+ mHourlyChartView.setVisibility(visibility);
+ }
+ }
+ };
+ }
+
+ private void addFooterPreferenceIfNeeded(boolean containAppItems) {
+ if (mIsFooterPrefAdded || mFooterPreference == null) {
+ return;
+ }
+ mIsFooterPrefAdded = true;
+ mFooterPreference.setTitle(mPrefContext.getString(
+ containAppItems
+ ? R.string.battery_usage_screen_footer
+ : R.string.battery_usage_screen_footer_empty));
+ mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
+ }
+
+ private boolean isBatteryLevelDataInOneDay() {
+ return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
+ }
+
+ private boolean isAllSelected() {
+ return (isBatteryLevelDataInOneDay()
+ || mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
+ && mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
+ }
+
+ @VisibleForTesting
+ static int getTotalHours(final BatteryLevelData batteryLevelData) {
+ if (batteryLevelData == null) {
+ return 0;
+ }
+ List dailyTimestamps = batteryLevelData.getDailyBatteryLevels().getTimestamps();
+ return (int) ((dailyTimestamps.get(dailyTimestamps.size() - 1) - dailyTimestamps.get(0))
+ / DateUtils.HOUR_IN_MILLIS);
+ }
+
+ /** Used for {@link AppBatteryPreferenceController}. */
+ public static List getAppBatteryUsageData(Context context) {
+ final long start = System.currentTimeMillis();
+ final Map> batteryHistoryMap =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getBatteryHistorySinceLastFullCharge(context);
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ return null;
+ }
+ Log.d(TAG, String.format("getBatterySinceLastFullChargeUsageData() size=%d time=%d/ms",
+ batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
+
+ final Map> batteryUsageData =
+ DataProcessor.getBatteryUsageData(context, batteryHistoryMap);
+ if (batteryUsageData == null) {
+ return null;
+ }
+ BatteryDiffData allBatteryDiffData = batteryUsageData.get(
+ BatteryChartViewModel.SELECTED_INDEX_ALL).get(
+ BatteryChartViewModel.SELECTED_INDEX_ALL);
+ return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList();
+ }
+
+ /** Used for {@link AppBatteryPreferenceController}. */
+ public static BatteryDiffEntry getAppBatteryUsageData(
+ Context context, String packageName, int userId) {
+ if (packageName == null) {
+ return null;
+ }
+ final List entries = getAppBatteryUsageData(context);
+ if (entries == null) {
+ return null;
+ }
+ for (BatteryDiffEntry entry : entries) {
+ final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
+ if (batteryHistEntry != null
+ && batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+ && batteryHistEntry.mUserId == userId
+ && packageName.equals(entry.getPackageName())) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ private final class DailyChartLabelTextGenerator implements
+ BatteryChartViewModel.LabelTextGenerator {
+ @Override
+ public String generateText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeDayOfWeek(mContext,
+ timestamps.get(index), /* isAbbreviation= */ true);
+ }
+
+ @Override
+ public String generateFullText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeDayOfWeek(mContext,
+ timestamps.get(index), /* isAbbreviation= */ false);
+ }
+ }
+
+ private final class HourlyChartLabelTextGenerator implements
+ BatteryChartViewModel.LabelTextGenerator {
+ @Override
+ public String generateText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeHour(mContext, timestamps.get(index),
+ mIs24HourFormat);
+ }
+
+ @Override
+ public String generateFullText(List timestamps, int index) {
+ return index == timestamps.size() - 1
+ ? generateText(timestamps, index)
+ : String.format("%s%s%s", generateText(timestamps, index),
+ mIs24HourFormat ? "-" : " - ", generateText(timestamps, index + 1));
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
new file mode 100644
index 00000000000..f84ced76d88
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.android.settings.Utils.formatPercentage;
+
+import static java.lang.Math.round;
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.widget.AppCompatImageView;
+
+import com.android.settings.R;
+import com.android.settingslib.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** A widget component to draw chart graph. */
+public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
+ private static final String TAG = "BatteryChartView";
+
+ private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
+ private static final long UPDATE_STATE_DELAYED_TIME = 500L;
+
+ /** A callback listener for selected group index is updated. */
+ public interface OnSelectListener {
+ /** The callback function for selected group index is updated. */
+ void onSelect(int trapezoidIndex);
+ }
+
+ private final String[] mPercentages = getPercentages();
+ private final Rect mIndent = new Rect();
+ private final Rect[] mPercentageBounds = new Rect[]{new Rect(), new Rect(), new Rect()};
+ private final List mAxisLabelsBounds = new ArrayList<>();
+
+ private BatteryChartViewModel mViewModel;
+ private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ private int mDividerWidth;
+ private int mDividerHeight;
+ private float mTrapezoidVOffset;
+ private float mTrapezoidHOffset;
+ private int mTrapezoidColor;
+ private int mTrapezoidSolidColor;
+ private int mTrapezoidHoverColor;
+ private int mTextPadding;
+ private Paint mDividerPaint;
+ private Paint mTrapezoidPaint;
+ private Paint mTextPaint;
+ private AccessibilityNodeProvider mAccessibilityNodeProvider;
+ private BatteryChartView.OnSelectListener mOnSelectListener;
+
+ @VisibleForTesting
+ TrapezoidSlot[] mTrapezoidSlots;
+ // Records the location to calculate selected index.
+ @VisibleForTesting
+ float mTouchUpEventX = Float.MIN_VALUE;
+
+ public BatteryChartView(Context context) {
+ super(context, null);
+ }
+
+ public BatteryChartView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initializeColors(context);
+ // Registers the click event listener.
+ setOnClickListener(this);
+ setClickable(false);
+ requestLayout();
+ }
+
+ /** Sets the data model of this view. */
+ public void setViewModel(BatteryChartViewModel viewModel) {
+ if (viewModel == null) {
+ mViewModel = null;
+ invalidate();
+ return;
+ }
+
+ Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
+ viewModel.size(), viewModel.selectedIndex()));
+ mViewModel = viewModel;
+ initializeAxisLabelsBounds();
+ initializeTrapezoidSlots(viewModel.size() - 1);
+ setClickable(hasAnyValidTrapezoid(viewModel));
+ requestLayout();
+ }
+
+ /** Sets the callback to monitor the selected group index. */
+ public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
+ mOnSelectListener = listener;
+ }
+
+ /** Sets the companion {@link TextView} for percentage information. */
+ public void setCompanionTextView(TextView textView) {
+ if (textView != null) {
+ // Pre-draws the view first to load style atttributions into paint.
+ textView.draw(new Canvas());
+ mTextPaint = textView.getPaint();
+ } else {
+ mTextPaint = null;
+ }
+ requestLayout();
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // Measures text bounds and updates indent configuration.
+ if (mTextPaint != null) {
+ mTextPaint.setTextAlign(Paint.Align.LEFT);
+ for (int index = 0; index < mPercentages.length; index++) {
+ mTextPaint.getTextBounds(
+ mPercentages[index], 0, mPercentages[index].length(),
+ mPercentageBounds[index]);
+ }
+ // Updates the indent configurations.
+ mIndent.top = mPercentageBounds[0].height();
+ mIndent.right = mPercentageBounds[0].width() + mTextPadding;
+
+ if (mViewModel != null) {
+ int maxTop = 0;
+ for (int index = 0; index < mViewModel.size(); index++) {
+ final String text = mViewModel.getText(index);
+ mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
+ maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
+ }
+ mIndent.bottom = maxTop + round(mTextPadding * 2f);
+ }
+ Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
+ } else {
+ mIndent.set(0, 0, 0, 0);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ // Before mLevels initialized, the count of trapezoids is unknown. Only draws the
+ // horizontal percentages and dividers.
+ drawHorizontalDividers(canvas);
+ if (mViewModel == null) {
+ return;
+ }
+ drawVerticalDividers(canvas);
+ drawTrapezoids(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Caches the location to calculate selected trapezoid index.
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ mTouchUpEventX = event.getX();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mTouchUpEventX = Float.MIN_VALUE; // reset
+ break;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ final int trapezoidIndex = getTrapezoidIndex(event.getX());
+ if (mHoveredIndex != trapezoidIndex) {
+ mHoveredIndex = trapezoidIndex;
+ invalidate();
+ sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ }
+ // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
+ // sent here.
+ return true;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) {
+ sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
+ invalidate();
+ }
+ // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
+ // sent here.
+ return true;
+ default:
+ return super.onTouchEvent(event);
+ }
+ }
+
+ @Override
+ public void onHoverChanged(boolean hovered) {
+ super.onHoverChanged(hovered);
+ if (!hovered) {
+ mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mTouchUpEventX == Float.MIN_VALUE) {
+ Log.w(TAG, "invalid motion event for onClick() callback");
+ return;
+ }
+ onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX));
+ }
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ if (mViewModel == null) {
+ return super.getAccessibilityNodeProvider();
+ }
+ if (mAccessibilityNodeProvider == null) {
+ mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider();
+ }
+ return mAccessibilityNodeProvider;
+ }
+
+ private void onTrapezoidClicked(View view, int index) {
+ // Ignores the click event if the level is zero.
+ if (!isValidToDraw(mViewModel, index)) {
+ return;
+ }
+ if (mOnSelectListener != null) {
+ // Selects all if users click the same trapezoid item two times.
+ mOnSelectListener.onSelect(
+ index == mViewModel.selectedIndex()
+ ? BatteryChartViewModel.SELECTED_INDEX_ALL : index);
+ }
+ view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
+ }
+
+ private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) {
+ ViewParent parent = getParent();
+ if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return false;
+ }
+ AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType);
+ accessibilityEvent.setSource(this, virtualDescendantId);
+ accessibilityEvent.setEnabled(true);
+ accessibilityEvent.setClassName(getAccessibilityClassName());
+ accessibilityEvent.setPackageName(getContext().getPackageName());
+ return parent.requestSendAccessibilityEvent(this, accessibilityEvent);
+ }
+
+ private void sendAccessibilityEventForHover(int eventType) {
+ if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) {
+ sendAccessibilityEvent(mHoveredIndex, eventType);
+ }
+ }
+
+ private void initializeTrapezoidSlots(int count) {
+ mTrapezoidSlots = new TrapezoidSlot[count];
+ for (int index = 0; index < mTrapezoidSlots.length; index++) {
+ mTrapezoidSlots[index] = new TrapezoidSlot();
+ }
+ }
+
+ private void initializeColors(Context context) {
+ setBackgroundColor(Color.TRANSPARENT);
+ mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
+ mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
+ mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
+ com.android.internal.R.attr.colorAccentSecondaryVariant);
+ // Initializes the divider line paint.
+ final Resources resources = getContext().getResources();
+ mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
+ mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
+ mDividerPaint = new Paint();
+ mDividerPaint.setAntiAlias(true);
+ mDividerPaint.setColor(DIVIDER_COLOR);
+ mDividerPaint.setStyle(Paint.Style.STROKE);
+ mDividerPaint.setStrokeWidth(mDividerWidth);
+ Log.i(TAG, "mDividerWidth:" + mDividerWidth);
+ Log.i(TAG, "mDividerHeight:" + mDividerHeight);
+ // Initializes the trapezoid paint.
+ mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
+ mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
+ mTrapezoidPaint = new Paint();
+ mTrapezoidPaint.setAntiAlias(true);
+ mTrapezoidPaint.setColor(mTrapezoidSolidColor);
+ mTrapezoidPaint.setStyle(Paint.Style.FILL);
+ mTrapezoidPaint.setPathEffect(
+ new CornerPathEffect(
+ resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
+ // Initializes for drawing text information.
+ mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
+ }
+
+ private void drawHorizontalDividers(Canvas canvas) {
+ final int width = getWidth() - mIndent.right;
+ final int height = getHeight() - mIndent.top - mIndent.bottom;
+ // Draws the top divider line for 100% curve.
+ float offsetY = mIndent.top + mDividerWidth * .5f;
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 0, offsetY);
+
+ // Draws the center divider line for 50% curve.
+ final float availableSpace =
+ height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
+ offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 1, offsetY);
+
+ // Draws the bottom divider line for 0% curve.
+ offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 2, offsetY);
+ }
+
+ private void drawPercentage(Canvas canvas, int index, float offsetY) {
+ if (mTextPaint != null) {
+ mTextPaint.setTextAlign(Paint.Align.RIGHT);
+ canvas.drawText(
+ mPercentages[index],
+ getWidth(),
+ offsetY + mPercentageBounds[index].height() * .5f,
+ mTextPaint);
+ }
+ }
+
+ private void drawVerticalDividers(Canvas canvas) {
+ final int width = getWidth() - mIndent.right;
+ final int dividerCount = mTrapezoidSlots.length + 1;
+ final float dividerSpace = dividerCount * mDividerWidth;
+ final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
+ final float bottomY = getHeight() - mIndent.bottom;
+ final float startY = bottomY - mDividerHeight;
+ final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
+ // Draws each vertical dividers.
+ float startX = mDividerWidth * .5f;
+ for (int index = 0; index < dividerCount; index++) {
+ canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
+ final float nextX = startX + mDividerWidth + unitWidth;
+ // Updates the trapezoid slots for drawing.
+ if (index < mTrapezoidSlots.length) {
+ mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
+ mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
+ }
+ startX = nextX;
+ }
+ // Draws the axis label slot information.
+ if (mViewModel != null) {
+ final float baselineY = getHeight() - mTextPadding;
+ Rect[] axisLabelDisplayAreas;
+ switch (mViewModel.axisLabelPosition()) {
+ case CENTER_OF_TRAPEZOIDS:
+ axisLabelDisplayAreas = getAxisLabelDisplayAreas(
+ /* size= */ mViewModel.size() - 1,
+ /* baselineX= */ mDividerWidth + unitWidth * .5f,
+ /* offsetX= */ mDividerWidth + unitWidth,
+ baselineY,
+ /* shiftFirstAndLast= */ false);
+ break;
+ case BETWEEN_TRAPEZOIDS:
+ default:
+ axisLabelDisplayAreas = getAxisLabelDisplayAreas(
+ /* size= */ mViewModel.size(),
+ /* baselineX= */ mDividerWidth * .5f,
+ /* offsetX= */ mDividerWidth + unitWidth,
+ baselineY,
+ /* shiftFirstAndLast= */ true);
+ break;
+ }
+ drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
+ }
+ }
+
+ /** Gets all the axis label texts displaying area positions if they are shown. */
+ private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX,
+ final float offsetX, final float baselineY, final boolean shiftFirstAndLast) {
+ final Rect[] result = new Rect[size];
+ for (int index = 0; index < result.length; index++) {
+ final float width = mAxisLabelsBounds.get(index).width();
+ float middle = baselineX + index * offsetX;
+ if (shiftFirstAndLast) {
+ if (index == 0) {
+ middle += width * .5f;
+ }
+ if (index == size - 1) {
+ middle -= width * .5f;
+ }
+ }
+ final float left = middle - width * .5f;
+ final float right = left + width;
+ final float top = baselineY + mAxisLabelsBounds.get(index).top;
+ final float bottom = top + mAxisLabelsBounds.get(index).height();
+ result[index] = new Rect(round(left), round(top), round(right), round(bottom));
+ }
+ return result;
+ }
+
+ private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
+ final int lastIndex = displayAreas.length - 1;
+ // Suppose first and last labels are always able to draw.
+ drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
+ drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
+ }
+
+ /**
+ * Recursively draws axis labels between the start index and the end index. If the inner number
+ * can be exactly divided into 2 parts, check and draw the middle index label and then
+ * recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
+ * labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
+ * back to the uplevel of the recursion.
+ */
+ private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas,
+ final Rect[] displayAreas, final int startIndex, final int endIndex,
+ final float baselineY) {
+ if (endIndex - startIndex <= 1) {
+ return;
+ }
+ if ((endIndex - startIndex) % 2 == 0) {
+ int middleIndex = (startIndex + endIndex) / 2;
+ if (hasOverlap(displayAreas, startIndex, middleIndex)
+ || hasOverlap(displayAreas, middleIndex, endIndex)) {
+ return;
+ }
+ drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(
+ canvas, displayAreas, startIndex, middleIndex, baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(
+ canvas, displayAreas, middleIndex, endIndex, baselineY);
+ } else {
+ int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
+ int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
+ if (hasOverlap(displayAreas, startIndex, middleIndex1)
+ || hasOverlap(displayAreas, middleIndex1, middleIndex2)
+ || hasOverlap(displayAreas, middleIndex2, endIndex)) {
+ return;
+ }
+ drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
+ drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(
+ canvas, displayAreas, startIndex, middleIndex1, baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(
+ canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
+ drawAxisLabelsBetweenStartIndexAndEndIndex(
+ canvas, displayAreas, middleIndex2, endIndex, baselineY);
+ }
+ }
+
+ private boolean hasOverlap(
+ final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
+ return displayAreas[leftIndex].right + mTextPadding * 2.3f > displayAreas[rightIndex].left;
+ }
+
+ private void drawAxisLabelText(
+ Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText(
+ mViewModel.getText(index),
+ displayArea.centerX(),
+ baselineY,
+ mTextPaint);
+ }
+
+ private void drawTrapezoids(Canvas canvas) {
+ // Ignores invalid trapezoid data.
+ if (mViewModel == null) {
+ return;
+ }
+ final float trapezoidBottom =
+ getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
+ - mTrapezoidVOffset;
+ final float availableSpace =
+ trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
+ final float unitHeight = availableSpace / 100f;
+ // Draws all trapezoid shapes into the canvas.
+ final Path trapezoidPath = new Path();
+ Path trapezoidCurvePath = null;
+ for (int index = 0; index < mTrapezoidSlots.length; index++) {
+ // Not draws the trapezoid for corner or not initialization cases.
+ if (!isValidToDraw(mViewModel, index)) {
+ continue;
+ }
+ // Configures the trapezoid paint color.
+ final int trapezoidColor = (mViewModel.selectedIndex() == index
+ || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
+ ? mTrapezoidSolidColor : mTrapezoidColor;
+ final boolean isHoverState = mHoveredIndex == index && isValidToDraw(mViewModel,
+ mHoveredIndex);
+ mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
+
+ final float leftTop = round(
+ trapezoidBottom - requireNonNull(mViewModel.getLevel(index)) * unitHeight);
+ final float rightTop = round(trapezoidBottom
+ - requireNonNull(mViewModel.getLevel(index + 1)) * unitHeight);
+ trapezoidPath.reset();
+ trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
+ // A tricky way to make the trapezoid shape drawing the rounded corner.
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
+ // Draws the trapezoid shape into canvas.
+ canvas.drawPath(trapezoidPath, mTrapezoidPaint);
+ }
+ }
+
+ // Searches the corresponding trapezoid index from x location.
+ private int getTrapezoidIndex(float x) {
+ if (mTrapezoidSlots == null) {
+ return BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ }
+ for (int index = 0; index < mTrapezoidSlots.length; index++) {
+ final TrapezoidSlot slot = mTrapezoidSlots[index];
+ if (x >= slot.mLeft - mTrapezoidHOffset
+ && x <= slot.mRight + mTrapezoidHOffset) {
+ return index;
+ }
+ }
+ return BatteryChartViewModel.SELECTED_INDEX_INVALID;
+ }
+
+ private void initializeAxisLabelsBounds() {
+ mAxisLabelsBounds.clear();
+ for (int i = 0; i < mViewModel.size(); i++) {
+ mAxisLabelsBounds.add(new Rect());
+ }
+ }
+
+ private static boolean isTrapezoidValid(
+ @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
+ return viewModel.getLevel(trapezoidIndex) != null
+ && viewModel.getLevel(trapezoidIndex + 1) != null;
+ }
+
+ private static boolean isTrapezoidIndexValid(
+ @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
+ return viewModel != null
+ && trapezoidIndex >= 0
+ && trapezoidIndex < viewModel.size() - 1;
+ }
+
+ private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
+ return isTrapezoidIndexValid(viewModel, trapezoidIndex)
+ && isTrapezoidValid(viewModel, trapezoidIndex);
+ }
+
+ private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
+ // Sets the chart is clickable if there is at least one valid item in it.
+ for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
+ if (isTrapezoidValid(viewModel, trapezoidIndex)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String[] getPercentages() {
+ return new String[]{
+ formatPercentage(/*percentage=*/ 100, /*round=*/ true),
+ formatPercentage(/*percentage=*/ 50, /*round=*/ true),
+ formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
+ }
+
+ private class BatteryChartAccessibilityNodeProvider extends AccessibilityNodeProvider {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ final AccessibilityNodeInfo hostInfo =
+ new AccessibilityNodeInfo(BatteryChartView.this);
+ for (int index = 0; index < mViewModel.size() - 1; index++) {
+ hostInfo.addChild(BatteryChartView.this, index);
+ }
+ return hostInfo;
+ }
+ final int index = virtualViewId;
+ if (!isTrapezoidIndexValid(mViewModel, index)) {
+ Log.w(TAG, "Invalid virtual view id:" + index);
+ return null;
+ }
+ final AccessibilityNodeInfo childInfo =
+ new AccessibilityNodeInfo(BatteryChartView.this, index);
+ onInitializeAccessibilityNodeInfo(childInfo);
+ childInfo.setClickable(isValidToDraw(mViewModel, index));
+ childInfo.setText(mViewModel.getFullText(index));
+ childInfo.setContentDescription(mViewModel.getFullText(index));
+
+ final Rect bounds = new Rect();
+ getBoundsOnScreen(bounds, true);
+ final int hostLeft = bounds.left;
+ bounds.left = round(hostLeft + mTrapezoidSlots[index].mLeft);
+ bounds.right = round(hostLeft + mTrapezoidSlots[index].mRight);
+ childInfo.setBoundsInScreen(bounds);
+ return childInfo;
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action,
+ @Nullable Bundle arguments) {
+ if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ return performAccessibilityAction(action, arguments);
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK:
+ onTrapezoidClicked(BatteryChartView.this, virtualViewId);
+ return true;
+
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+ return sendAccessibilityEvent(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ return sendAccessibilityEvent(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+
+ default:
+ return performAccessibilityAction(action, arguments);
+ }
+ }
+ }
+
+ // A container class for each trapezoid left and right location.
+ @VisibleForTesting
+ static final class TrapezoidSlot {
+ public float mLeft;
+ public float mRight;
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
new file mode 100644
index 00000000000..f58d2415e19
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/** The view model of {@code BatteryChartView} */
+class BatteryChartViewModel {
+ private static final String TAG = "BatteryChartViewModel";
+
+ public static final int SELECTED_INDEX_ALL = -1;
+ public static final int SELECTED_INDEX_INVALID = -2;
+
+ // We need at least 2 levels to draw a trapezoid.
+ private static final int MIN_LEVELS_DATA_SIZE = 2;
+
+ enum AxisLabelPosition {
+ BETWEEN_TRAPEZOIDS,
+ CENTER_OF_TRAPEZOIDS,
+ }
+
+ interface LabelTextGenerator {
+ /** Generate the label text. The text may be abbreviated to save space. */
+ String generateText(List timestamps, int index);
+
+ /** Generate the full text for accessibility. */
+ String generateFullText(List timestamps, int index);
+ }
+
+ private final List mLevels;
+ private final List mTimestamps;
+ private final AxisLabelPosition mAxisLabelPosition;
+ private final LabelTextGenerator mLabelTextGenerator;
+ private final String[] mTexts;
+ private final String[] mFullTexts;
+
+ private int mSelectedIndex = SELECTED_INDEX_ALL;
+
+ BatteryChartViewModel(@NonNull List levels, @NonNull List timestamps,
+ @NonNull AxisLabelPosition axisLabelPosition,
+ @NonNull LabelTextGenerator labelTextGenerator) {
+ Preconditions.checkArgument(
+ levels.size() == timestamps.size() && levels.size() >= MIN_LEVELS_DATA_SIZE,
+ String.format(Locale.ENGLISH,
+ "Invalid BatteryChartViewModel levels.size: %d, timestamps.size: %d.",
+ levels.size(), timestamps.size()));
+ mLevels = levels;
+ mTimestamps = timestamps;
+ mAxisLabelPosition = axisLabelPosition;
+ mLabelTextGenerator = labelTextGenerator;
+ mTexts = new String[size()];
+ mFullTexts = new String[size()];
+ }
+
+ public int size() {
+ return mLevels.size();
+ }
+
+ public Integer getLevel(int index) {
+ return mLevels.get(index);
+ }
+
+ public String getText(int index) {
+ if (mTexts[index] == null) {
+ mTexts[index] = mLabelTextGenerator.generateText(mTimestamps, index);
+ }
+ return mTexts[index];
+ }
+
+ public String getFullText(int index) {
+ if (mFullTexts[index] == null) {
+ mFullTexts[index] = mLabelTextGenerator.generateFullText(mTimestamps, index);
+ }
+ return mFullTexts[index];
+ }
+
+ public AxisLabelPosition axisLabelPosition() {
+ return mAxisLabelPosition;
+ }
+
+ public int selectedIndex() {
+ return mSelectedIndex;
+ }
+
+ public void setSelectedIndex(int index) {
+ mSelectedIndex = index;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLevels, mTimestamps, mSelectedIndex, mAxisLabelPosition);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof BatteryChartViewModel)) {
+ return false;
+ }
+ final BatteryChartViewModel batteryChartViewModel = (BatteryChartViewModel) other;
+ return Objects.equals(mLevels, batteryChartViewModel.mLevels)
+ && Objects.equals(mTimestamps, batteryChartViewModel.mTimestamps)
+ && mAxisLabelPosition == batteryChartViewModel.mAxisLabelPosition
+ && mSelectedIndex == batteryChartViewModel.mSelectedIndex;
+ }
+
+ @Override
+ public String toString() {
+ // Generate all the texts and full texts.
+ for (int i = 0; i < size(); i++) {
+ getText(i);
+ getFullText(i);
+ }
+
+ return new StringBuilder()
+ .append("levels: " + Objects.toString(mLevels))
+ .append(", timestamps: " + Objects.toString(mTimestamps))
+ .append(", texts: " + Arrays.toString(mTexts))
+ .append(", fullTexts: " + Arrays.toString(mFullTexts))
+ .append(", axisLabelPosition: " + mAxisLabelPosition)
+ .append(", selectedIndex: " + mSelectedIndex)
+ .toString();
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
new file mode 100644
index 00000000000..b5d4dde883c
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Wraps the battery usage diff data for each entry used for battery usage app list. */
+public class BatteryDiffData {
+ private final List mAppEntries;
+ private final List mSystemEntries;
+
+ /** Constructor for the diff entries which already have totalConsumePower value. */
+ public BatteryDiffData(
+ @NonNull List appDiffEntries,
+ @NonNull List systemDiffEntries) {
+ mAppEntries = appDiffEntries;
+ mSystemEntries = systemDiffEntries;
+ sortEntries();
+ }
+
+ /** Constructor for the diff entries which have not set totalConsumePower value. */
+ public BatteryDiffData(
+ @NonNull List appDiffEntries,
+ @NonNull List systemDiffEntries,
+ final double totalConsumePower) {
+ mAppEntries = appDiffEntries;
+ mSystemEntries = systemDiffEntries;
+ setTotalConsumePowerForAllEntries(totalConsumePower);
+ sortEntries();
+ }
+
+ public List getAppDiffEntryList() {
+ return mAppEntries;
+ }
+
+ public List getSystemDiffEntryList() {
+ return mSystemEntries;
+ }
+
+ // Sets total consume power for each entry.
+ private void setTotalConsumePowerForAllEntries(final double totalConsumePower) {
+ mAppEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
+ mSystemEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
+ }
+
+ // Sorts entries based on consumed percentage.
+ private void sortEntries() {
+ Collections.sort(mAppEntries, BatteryDiffEntry.COMPARATOR);
+ Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
similarity index 71%
rename from src/com/android/settings/fuelgauge/BatteryDiffEntry.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
index 403f79cffa4..d4d7a00fdc2 100644
--- a/src/com/android/settings/fuelgauge/BatteryDiffEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
@@ -1,17 +1,19 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -26,6 +28,7 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
+import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settingslib.utils.StringUtil;
import java.util.Comparator;
@@ -61,11 +64,16 @@ public class BatteryDiffEntry {
private UserManager mUserManager;
private String mDefaultPackageName = null;
- @VisibleForTesting int mAppIconId;
- @VisibleForTesting String mAppLabel = null;
- @VisibleForTesting Drawable mAppIcon = null;
- @VisibleForTesting boolean mIsLoaded = false;
- @VisibleForTesting boolean mValidForRestriction = true;
+ @VisibleForTesting
+ int mAppIconId;
+ @VisibleForTesting
+ String mAppLabel = null;
+ @VisibleForTesting
+ Drawable mAppIcon = null;
+ @VisibleForTesting
+ boolean mIsLoaded = false;
+ @VisibleForTesting
+ boolean mValidForRestriction = true;
public BatteryDiffEntry(
Context context,
@@ -85,7 +93,7 @@ public class BatteryDiffEntry {
public void setTotalConsumePower(double totalConsumePower) {
mTotalConsumePower = totalConsumePower;
mPercentOfTotal = totalConsumePower == 0
- ? 0 : (mConsumePower / mTotalConsumePower) * 100.0;
+ ? 0 : (mConsumePower / mTotalConsumePower) * 100.0;
}
/** Gets the percentage of total consumed power. */
@@ -96,24 +104,30 @@ public class BatteryDiffEntry {
/** Clones a new instance. */
public BatteryDiffEntry clone() {
return new BatteryDiffEntry(
- this.mContext,
- this.mForegroundUsageTimeInMs,
- this.mBackgroundUsageTimeInMs,
- this.mConsumePower,
- this.mBatteryHistEntry /*same instance*/);
+ this.mContext,
+ this.mForegroundUsageTimeInMs,
+ this.mBackgroundUsageTimeInMs,
+ this.mConsumePower,
+ this.mBatteryHistEntry /*same instance*/);
}
/** Gets the app label name for this entry. */
public String getAppLabel() {
+ if (isOtherUsers()) {
+ return mContext.getString(R.string.battery_usage_other_users);
+ }
loadLabelAndIcon();
// Returns default applicationn label if we cannot find it.
return mAppLabel == null || mAppLabel.length() == 0
- ? mBatteryHistEntry.mAppLabel
- : mAppLabel;
+ ? mBatteryHistEntry.mAppLabel
+ : mAppLabel;
}
/** Gets the app icon {@link Drawable} for this entry. */
public Drawable getAppIcon() {
+ if (isOtherUsers()) {
+ return mContext.getDrawable(R.drawable.ic_power_system);
+ }
loadLabelAndIcon();
return mAppIcon != null && mAppIcon.getConstantState() != null
? mAppIcon.getConstantState().newDrawable()
@@ -129,15 +143,15 @@ public class BatteryDiffEntry {
/** Gets the searching package name for UID battery type. */
public String getPackageName() {
final String packageName = mDefaultPackageName != null
- ? mDefaultPackageName : mBatteryHistEntry.mPackageName;
+ ? mDefaultPackageName : mBatteryHistEntry.mPackageName;
if (packageName == null) {
return packageName;
}
// Removes potential appended process name in the PackageName.
// From "com.opera.browser:privileged_process0" to "com.opera.browser"
- final String[] splittedPackageNames = packageName.split(":");
- return splittedPackageNames != null && splittedPackageNames.length > 0
- ? splittedPackageNames[0] : packageName;
+ final String[] splitPackageNames = packageName.split(":");
+ return splitPackageNames != null && splitPackageNames.length > 0
+ ? splitPackageNames[0] : packageName;
}
/** Whether this item is valid for users to launch restriction page? */
@@ -148,6 +162,9 @@ public class BatteryDiffEntry {
/** Whether the current BatteryDiffEntry is system component or not. */
public boolean isSystemEntry() {
+ if (isOtherUsers()) {
+ return true;
+ }
switch (mBatteryHistEntry.mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
@@ -167,6 +184,11 @@ public class BatteryDiffEntry {
return false;
}
+ private boolean isOtherUsers() {
+ return mBatteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+ && mBatteryHistEntry.mUid == BatteryUtils.UID_OTHER_USERS;
+ }
+
void loadLabelAndIcon() {
if (mIsLoaded) {
return;
@@ -196,20 +218,20 @@ public class BatteryDiffEntry {
switch (mBatteryHistEntry.mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
final BatteryEntry.NameAndIcon nameAndIconForUser =
- BatteryEntry.getNameAndIconFromUserId(
- mContext, (int) mBatteryHistEntry.mUserId);
+ BatteryEntry.getNameAndIconFromUserId(
+ mContext, (int) mBatteryHistEntry.mUserId);
if (nameAndIconForUser != null) {
mAppIcon = nameAndIconForUser.mIcon;
mAppLabel = nameAndIconForUser.mName;
sResourceCache.put(
- getKey(),
- new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
+ getKey(),
+ new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
}
break;
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
final BatteryEntry.NameAndIcon nameAndIconForSystem =
- BatteryEntry.getNameAndIconFromPowerComponent(
- mContext, mBatteryHistEntry.mDrainType);
+ BatteryEntry.getNameAndIconFromPowerComponent(
+ mContext, mBatteryHistEntry.mDrainType);
if (nameAndIconForSystem != null) {
mAppLabel = nameAndIconForSystem.mName;
if (nameAndIconForSystem.mIconId != 0) {
@@ -217,8 +239,8 @@ public class BatteryDiffEntry {
mAppIcon = mContext.getDrawable(nameAndIconForSystem.mIconId);
}
sResourceCache.put(
- getKey(),
- new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
+ getKey(),
+ new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
}
break;
case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
@@ -231,8 +253,8 @@ public class BatteryDiffEntry {
mAppIcon = getBadgeIconForUser(mAppIcon);
if (mAppLabel != null || mAppIcon != null) {
sResourceCache.put(
- getKey(),
- new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
+ getKey(),
+ new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
}
break;
}
@@ -251,23 +273,23 @@ public class BatteryDiffEntry {
}
final boolean isValidPackage =
BatteryUtils.getInstance(mContext).getPackageUid(getPackageName())
- != BatteryUtils.UID_NULL;
+ != BatteryUtils.UID_NULL;
if (!isValidPackage) {
mValidForRestriction = false;
return;
}
try {
mValidForRestriction =
- mContext.getPackageManager().getPackageInfo(
- getPackageName(),
- PackageManager.MATCH_DISABLED_COMPONENTS
- | PackageManager.MATCH_ANY_USER
- | PackageManager.GET_SIGNATURES
- | PackageManager.GET_PERMISSIONS)
- != null;
+ mContext.getPackageManager().getPackageInfo(
+ getPackageName(),
+ PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_ANY_USER
+ | PackageManager.GET_SIGNATURES
+ | PackageManager.GET_PERMISSIONS)
+ != null;
} catch (Exception e) {
Log.e(TAG, String.format("getPackageInfo() error %s for package=%s",
- e.getCause(), getPackageName()));
+ e.getCause(), getPackageName()));
mValidForRestriction = false;
}
}
@@ -276,7 +298,7 @@ public class BatteryDiffEntry {
final Locale locale = Locale.getDefault();
if (sCurrentLocale != locale) {
Log.d(TAG, String.format("clearCache() locale is changed from %s to %s",
- sCurrentLocale, locale));
+ sCurrentLocale, locale));
sCurrentLocale = locale;
clearCache();
}
@@ -290,7 +312,7 @@ public class BatteryDiffEntry {
if (packageName != null && packageName.length() != 0) {
try {
final ApplicationInfo appInfo =
- packageManager.getApplicationInfo(packageName, /*no flags*/ 0);
+ packageManager.getApplicationInfo(packageName, /*no flags*/ 0);
if (appInfo != null) {
mAppLabel = packageManager.getApplicationLabel(appInfo).toString();
mAppIcon = packageManager.getApplicationIcon(appInfo);
@@ -310,15 +332,15 @@ public class BatteryDiffEntry {
// Loads special defined application label and icon if available.
if (packages == null || packages.length == 0) {
final BatteryEntry.NameAndIcon nameAndIcon =
- BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid);
+ BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid);
mAppLabel = nameAndIcon.mName;
mAppIcon = nameAndIcon.mIcon;
}
final BatteryEntry.NameAndIcon nameAndIcon =
- BatteryEntry.loadNameAndIcon(
- mContext, uid, /*handler=*/ null, /*batteryEntry=*/ null,
- packageName, mAppLabel, mAppIcon);
+ BatteryEntry.loadNameAndIcon(
+ mContext, uid, /*handler=*/ null, /*batteryEntry=*/ null,
+ packageName, mAppLabel, mAppIcon);
// Clears BatteryEntry internal cache since we will have another one.
BatteryEntry.clearUidCache();
if (nameAndIcon != null) {
@@ -328,7 +350,7 @@ public class BatteryDiffEntry {
if (mDefaultPackageName != null
&& !mDefaultPackageName.equals(nameAndIcon.mPackageName)) {
Log.w(TAG, String.format("found different package: %s | %s",
- mDefaultPackageName, nameAndIcon.mPackageName));
+ mDefaultPackageName, nameAndIcon.mPackageName));
}
}
}
@@ -336,19 +358,19 @@ public class BatteryDiffEntry {
@Override
public String toString() {
final StringBuilder builder = new StringBuilder()
- .append("BatteryDiffEntry{")
- .append(String.format("\n\tname=%s restrictable=%b",
- mAppLabel, mValidForRestriction))
- .append(String.format("\n\tconsume=%.2f%% %f/%f",
- mPercentOfTotal, mConsumePower, mTotalConsumePower))
- .append(String.format("\n\tforeground:%s background:%s",
- StringUtil.formatElapsedTime(mContext, mForegroundUsageTimeInMs,
- /*withSeconds=*/ true, /*collapseTimeUnit=*/ false),
- StringUtil.formatElapsedTime(mContext, mBackgroundUsageTimeInMs,
- /*withSeconds=*/ true, /*collapseTimeUnit=*/ false)))
- .append(String.format("\n\tpackage:%s|%s uid:%d userId:%d",
- mBatteryHistEntry.mPackageName, getPackageName(),
- mBatteryHistEntry.mUid, mBatteryHistEntry.mUserId));
+ .append("BatteryDiffEntry{")
+ .append(String.format("\n\tname=%s restrictable=%b",
+ mAppLabel, mValidForRestriction))
+ .append(String.format("\n\tconsume=%.2f%% %f/%f",
+ mPercentOfTotal, mConsumePower, mTotalConsumePower))
+ .append(String.format("\n\tforeground:%s background:%s",
+ StringUtil.formatElapsedTime(mContext, mForegroundUsageTimeInMs,
+ /*withSeconds=*/ true, /*collapseTimeUnit=*/ false),
+ StringUtil.formatElapsedTime(mContext, mBackgroundUsageTimeInMs,
+ /*withSeconds=*/ true, /*collapseTimeUnit=*/ false)))
+ .append(String.format("\n\tpackage:%s|%s uid:%d userId:%d",
+ mBatteryHistEntry.mPackageName, getPackageName(),
+ mBatteryHistEntry.mUid, mBatteryHistEntry.mUserId));
return builder.toString();
}
@@ -361,7 +383,7 @@ public class BatteryDiffEntry {
private Drawable getBadgeIconForUser(Drawable icon) {
final int userId = UserHandle.getUserId((int) mBatteryHistEntry.mUid);
return userId == UserHandle.USER_OWNER ? icon :
- mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
+ mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
}
private static boolean isSystemUid(int uid) {
diff --git a/src/com/android/settings/fuelgauge/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
similarity index 92%
rename from src/com/android/settings/fuelgauge/BatteryEntry.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
index 9dcbd05564e..ccb2fb786eb 100644
--- a/src/com/android/settings/fuelgauge/BatteryEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.app.AppGlobals;
import android.content.Context;
@@ -36,9 +36,8 @@ import android.os.UserManager;
import android.util.DebugUtils;
import android.util.Log;
-import androidx.annotation.NonNull;
-
import com.android.settings.R;
+import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settingslib.Utils;
import java.util.ArrayList;
@@ -52,6 +51,7 @@ import java.util.Locale;
*/
public class BatteryEntry {
+ /** The app name and icon in app list. */
public static final class NameAndIcon {
public final String mName;
public final String mPackageName;
@@ -84,10 +84,10 @@ public class BatteryEntry {
static Locale sCurrentLocale = null;
- static private class NameAndIconLoader extends Thread {
+ private static class NameAndIconLoader extends Thread {
private boolean mAbort = false;
- public NameAndIconLoader() {
+ NameAndIconLoader() {
super("BatteryUsage Icon Loader");
}
@@ -109,9 +109,9 @@ public class BatteryEntry {
be = sRequestQueue.remove(0);
}
final NameAndIcon nameAndIcon =
- BatteryEntry.loadNameAndIcon(
- be.mContext, be.getUid(), sHandler, be,
- be.mDefaultPackageName, be.mName, be.mIcon);
+ BatteryEntry.loadNameAndIcon(
+ be.mContext, be.getUid(), sHandler, be,
+ be.mDefaultPackageName, be.mName, be.mIcon);
if (nameAndIcon != null) {
be.mIcon = nameAndIcon.mIcon;
be.mName = nameAndIcon.mName;
@@ -121,35 +121,38 @@ public class BatteryEntry {
}
}
- private static NameAndIconLoader mRequestThread;
+ private static NameAndIconLoader sRequestThread;
+ /** Starts the request queue. */
public static void startRequestQueue() {
if (sHandler != null) {
synchronized (sRequestQueue) {
if (!sRequestQueue.isEmpty()) {
- if (mRequestThread != null) {
- mRequestThread.abort();
+ if (sRequestThread != null) {
+ sRequestThread.abort();
}
- mRequestThread = new NameAndIconLoader();
- mRequestThread.setPriority(Thread.MIN_PRIORITY);
- mRequestThread.start();
+ sRequestThread = new NameAndIconLoader();
+ sRequestThread.setPriority(Thread.MIN_PRIORITY);
+ sRequestThread.start();
sRequestQueue.notify();
}
}
}
}
+ /** Stops the request queue. */
public static void stopRequestQueue() {
synchronized (sRequestQueue) {
- if (mRequestThread != null) {
- mRequestThread.abort();
- mRequestThread = null;
+ if (sRequestThread != null) {
+ sRequestThread.abort();
+ sRequestThread = null;
sRequestQueue.clear();
sHandler = null;
}
}
}
+ /** Clears the UID cache. */
public static void clearUidCache() {
sUidCache.clear();
}
@@ -252,14 +255,14 @@ public class BatteryEntry {
mIsHidden = false;
mPowerComponentId = powerComponentId;
mConsumedPower =
- powerComponentId == BatteryConsumer.POWER_COMPONENT_SCREEN
- ? devicePowerMah
- : devicePowerMah - appsPowerMah;
+ powerComponentId == BatteryConsumer.POWER_COMPONENT_SCREEN
+ ? devicePowerMah
+ : devicePowerMah - appsPowerMah;
mUsageDurationMs = usageDurationMs;
mConsumerType = ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
final NameAndIcon nameAndIcon =
- getNameAndIconFromPowerComponent(context, powerComponentId);
+ getNameAndIconFromPowerComponent(context, powerComponentId);
mIconId = nameAndIcon.mIconId;
mName = nameAndIcon.mName;
if (mIconId != 0) {
@@ -280,9 +283,9 @@ public class BatteryEntry {
mIcon = context.getDrawable(mIconId);
mName = powerComponentName;
mConsumedPower =
- powerComponentId == BatteryConsumer.POWER_COMPONENT_SCREEN
- ? devicePowerMah
- : devicePowerMah - appsPowerMah;
+ powerComponentId == BatteryConsumer.POWER_COMPONENT_SCREEN
+ ? devicePowerMah
+ : devicePowerMah - appsPowerMah;
mConsumerType = ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
}
@@ -354,7 +357,7 @@ public class BatteryEntry {
final PackageManager pm = context.getPackageManager();
final String[] packages = isSystemUid(uid)
- ? new String[] {PACKAGE_SYSTEM} : pm.getPackagesForUid(uid);
+ ? new String[]{PACKAGE_SYSTEM} : pm.getPackagesForUid(uid);
if (packages != null) {
final String[] packageLabels = new String[packages.length];
System.arraycopy(packages, 0, packageLabels, 0, packages.length);
@@ -552,8 +555,8 @@ public class BatteryEntry {
name = context.getResources().getString(R.string.process_network_tethering);
} else if ("mediaserver".equals(name)) {
name = context.getResources().getString(R.string.process_mediaserver_label);
- } else if ("dex2oat".equals(name) || "dex2oat32".equals(name) ||
- "dex2oat64".equals(name)) {
+ } else if ("dex2oat".equals(name) || "dex2oat32".equals(name)
+ || "dex2oat64".equals(name)) {
name = context.getResources().getString(R.string.process_dex2oat_label);
}
return new NameAndIcon(name, icon, 0 /* iconId */);
@@ -571,7 +574,7 @@ public class BatteryEntry {
break;
case BatteryConsumer.POWER_COMPONENT_BLUETOOTH:
name = context.getResources().getString(R.string.power_bluetooth);
- iconId = com.android.internal.R.drawable.ic_settings_bluetooth;
+ iconId = R.drawable.ic_settings_bluetooth;
break;
case BatteryConsumer.POWER_COMPONENT_CAMERA:
name = context.getResources().getString(R.string.power_camera);
@@ -595,7 +598,7 @@ public class BatteryEntry {
break;
case BatteryConsumer.POWER_COMPONENT_WIFI:
name = context.getResources().getString(R.string.power_wifi);
- iconId = R.drawable.ic_settings_wireless;
+ iconId = R.drawable.ic_settings_wireless_no_theme;
break;
case BatteryConsumer.POWER_COMPONENT_IDLE:
case BatteryConsumer.POWER_COMPONENT_MEMORY:
@@ -612,7 +615,8 @@ public class BatteryEntry {
return new NameAndIcon(name, null /* icon */, iconId);
}
- static boolean isSystemUid(int uid) {
+ /** Whether the uid is system uid. */
+ public static boolean isSystemUid(int uid) {
return uid == Process.SYSTEM_UID;
}
}
diff --git a/src/com/android/settings/fuelgauge/BatteryHistEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
similarity index 78%
rename from src/com/android/settings/fuelgauge/BatteryHistEntry.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
index 45f9e9f018c..b88b2f6ec25 100644
--- a/src/com/android/settings/fuelgauge/BatteryHistEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
@@ -1,17 +1,19 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.content.ContentValues;
import android.database.Cursor;
@@ -117,14 +119,14 @@ public class BatteryHistEntry {
}
private BatteryHistEntry(
- BatteryHistEntry fromEntry,
- long bootTimestamp,
- long timestamp,
- double totalPower,
- double consumePower,
- long foregroundUsageTimeInMs,
- long backgroundUsageTimeInMs,
- int batteryLevel) {
+ BatteryHistEntry fromEntry,
+ long bootTimestamp,
+ long timestamp,
+ double totalPower,
+ double consumePower,
+ long foregroundUsageTimeInMs,
+ long backgroundUsageTimeInMs,
+ int batteryLevel) {
mUid = fromEntry.mUid;
mUserId = fromEntry.mUserId;
mAppLabel = fromEntry.mAppLabel;
@@ -186,28 +188,28 @@ public class BatteryHistEntry {
@Override
public String toString() {
final String recordAtDateTime =
- ConvertUtils.utcToLocalTime(/*context=*/ null, mTimestamp);
+ ConvertUtils.utcToLocalTime(/*context=*/ null, mTimestamp);
final StringBuilder builder = new StringBuilder()
- .append("\nBatteryHistEntry{")
- .append(String.format("\n\tpackage=%s|label=%s|uid=%d|userId=%d|isHidden=%b",
- mPackageName, mAppLabel, mUid, mUserId, mIsHidden))
- .append(String.format("\n\ttimestamp=%s|zoneId=%s|bootTimestamp=%d",
- recordAtDateTime, mZoneId, Duration.ofMillis(mBootTimestamp).getSeconds()))
- .append(String.format("\n\tusage=%f|total=%f|consume=%f|elapsedTime=%d|%d",
- mPercentOfTotal, mTotalPower, mConsumePower,
- Duration.ofMillis(mForegroundUsageTimeInMs).getSeconds(),
- Duration.ofMillis(mBackgroundUsageTimeInMs).getSeconds()))
- .append(String.format("\n\tdrainType=%d|consumerType=%d",
- mDrainType, mConsumerType))
- .append(String.format("\n\tbattery=%d|status=%d|health=%d\n}",
- mBatteryLevel, mBatteryStatus, mBatteryHealth));
+ .append("\nBatteryHistEntry{")
+ .append(String.format("\n\tpackage=%s|label=%s|uid=%d|userId=%d|isHidden=%b",
+ mPackageName, mAppLabel, mUid, mUserId, mIsHidden))
+ .append(String.format("\n\ttimestamp=%s|zoneId=%s|bootTimestamp=%d",
+ recordAtDateTime, mZoneId, Duration.ofMillis(mBootTimestamp).getSeconds()))
+ .append(String.format("\n\tusage=%f|total=%f|consume=%f|elapsedTime=%d|%d",
+ mPercentOfTotal, mTotalPower, mConsumePower,
+ Duration.ofMillis(mForegroundUsageTimeInMs).getSeconds(),
+ Duration.ofMillis(mBackgroundUsageTimeInMs).getSeconds()))
+ .append(String.format("\n\tdrainType=%d|consumerType=%d",
+ mDrainType, mConsumerType))
+ .append(String.format("\n\tbattery=%d|status=%d|health=%d\n}",
+ mBatteryLevel, mBatteryStatus, mBatteryHealth));
return builder.toString();
}
private int getInteger(ContentValues values, String key) {
if (values != null && values.containsKey(key)) {
return values.getAsInteger(key);
- };
+ }
mIsValidEntry = false;
return 0;
}
@@ -298,21 +300,21 @@ public class BatteryHistEntry {
BatteryHistEntry lowerHistEntry,
BatteryHistEntry upperHistEntry) {
final double totalPower = interpolate(
- lowerHistEntry == null ? 0 : lowerHistEntry.mTotalPower,
- upperHistEntry.mTotalPower,
- ratio);
+ lowerHistEntry == null ? 0 : lowerHistEntry.mTotalPower,
+ upperHistEntry.mTotalPower,
+ ratio);
final double consumePower = interpolate(
- lowerHistEntry == null ? 0 : lowerHistEntry.mConsumePower,
- upperHistEntry.mConsumePower,
- ratio);
+ lowerHistEntry == null ? 0 : lowerHistEntry.mConsumePower,
+ upperHistEntry.mConsumePower,
+ ratio);
final double foregroundUsageTimeInMs = interpolate(
- lowerHistEntry == null ? 0 : lowerHistEntry.mForegroundUsageTimeInMs,
- upperHistEntry.mForegroundUsageTimeInMs,
- ratio);
+ lowerHistEntry == null ? 0 : lowerHistEntry.mForegroundUsageTimeInMs,
+ upperHistEntry.mForegroundUsageTimeInMs,
+ ratio);
final double backgroundUsageTimeInMs = interpolate(
- lowerHistEntry == null ? 0 : lowerHistEntry.mBackgroundUsageTimeInMs,
- upperHistEntry.mBackgroundUsageTimeInMs,
- ratio);
+ lowerHistEntry == null ? 0 : lowerHistEntry.mBackgroundUsageTimeInMs,
+ upperHistEntry.mBackgroundUsageTimeInMs,
+ ratio);
// Checks whether there is any abnoaml cases!
if (upperHistEntry.mConsumePower < consumePower
|| upperHistEntry.mForegroundUsageTimeInMs < foregroundUsageTimeInMs
@@ -324,22 +326,22 @@ public class BatteryHistEntry {
}
}
final double batteryLevel =
- lowerHistEntry == null
- ? upperHistEntry.mBatteryLevel
- : interpolate(
- lowerHistEntry.mBatteryLevel,
- upperHistEntry.mBatteryLevel,
- ratio);
+ lowerHistEntry == null
+ ? upperHistEntry.mBatteryLevel
+ : interpolate(
+ lowerHistEntry.mBatteryLevel,
+ upperHistEntry.mBatteryLevel,
+ ratio);
return new BatteryHistEntry(
- upperHistEntry,
- /*bootTimestamp=*/ upperHistEntry.mBootTimestamp
+ upperHistEntry,
+ /*bootTimestamp=*/ upperHistEntry.mBootTimestamp
- (upperTimestamp - slotTimestamp),
- /*timestamp=*/ slotTimestamp,
- totalPower,
- consumePower,
- Math.round(foregroundUsageTimeInMs),
- Math.round(backgroundUsageTimeInMs),
- (int) Math.round(batteryLevel));
+ /*timestamp=*/ slotTimestamp,
+ totalPower,
+ consumePower,
+ Math.round(foregroundUsageTimeInMs),
+ Math.round(backgroundUsageTimeInMs),
+ (int) Math.round(batteryLevel));
}
private static double interpolate(double v1, double v2, double ratio) {
diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
similarity index 79%
rename from src/com/android/settings/fuelgauge/BatteryHistoryLoader.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
index ddf3bf47d43..83b26150d39 100644
--- a/src/com/android/settings/fuelgauge/BatteryHistoryLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
+import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.utils.AsyncLoaderCompat;
@@ -41,7 +42,7 @@ public class BatteryHistoryLoader
@Override
public Map> loadInBackground() {
final PowerUsageFeatureProvider powerUsageFeatureProvider =
- FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext);
- return powerUsageFeatureProvider.getBatteryHistory(mContext);
+ FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext);
+ return powerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext);
}
}
diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
similarity index 72%
rename from src/com/android/settings/fuelgauge/BatteryHistoryPreference.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
index b2818bf601d..674822359be 100644
--- a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
import android.os.BatteryUsageStats;
@@ -29,6 +29,8 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.UsageView;
@@ -38,29 +40,33 @@ import com.android.settings.widget.UsageView;
public class BatteryHistoryPreference extends Preference {
private static final String TAG = "BatteryHistoryPreference";
- @VisibleForTesting boolean mHideSummary;
- @VisibleForTesting BatteryInfo mBatteryInfo;
+ @VisibleForTesting
+ boolean mHideSummary;
+ @VisibleForTesting
+ BatteryInfo mBatteryInfo;
private boolean mIsChartGraphEnabled;
private TextView mSummaryView;
private CharSequence mSummaryContent;
- private BatteryChartView mBatteryChartView;
+ private BatteryChartView mDailyChartView;
+ private BatteryChartView mHourlyChartView;
private BatteryChartPreferenceController mChartPreferenceController;
public BatteryHistoryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mIsChartGraphEnabled =
- FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context)
- .isChartGraphEnabled(context);
+ FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context)
+ .isChartGraphEnabled(context);
Log.i(TAG, "isChartGraphEnabled: " + mIsChartGraphEnabled);
setLayoutResource(
- mIsChartGraphEnabled
- ? R.layout.battery_chart_graph
- : R.layout.battery_usage_graph);
+ mIsChartGraphEnabled
+ ? R.layout.battery_chart_graph
+ : R.layout.battery_usage_graph);
setSelectable(false);
}
+ /** Sets the text of bottom summary. */
public void setBottomSummary(CharSequence text) {
mSummaryContent = text;
if (mSummaryView != null) {
@@ -70,6 +76,7 @@ public class BatteryHistoryPreference extends Preference {
mHideSummary = false;
}
+ /** Hides the bottom summary. */
public void hideBottomSummary() {
if (mSummaryView != null) {
mSummaryView.setVisibility(View.GONE);
@@ -86,8 +93,8 @@ public class BatteryHistoryPreference extends Preference {
void setChartPreferenceController(BatteryChartPreferenceController controller) {
mChartPreferenceController = controller;
- if (mBatteryChartView != null) {
- mChartPreferenceController.setBatteryChartView(mBatteryChartView);
+ if (mDailyChartView != null && mHourlyChartView != null) {
+ mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
}
@@ -99,11 +106,13 @@ public class BatteryHistoryPreference extends Preference {
return;
}
if (mIsChartGraphEnabled) {
- mBatteryChartView = (BatteryChartView) view.findViewById(R.id.battery_chart);
- mBatteryChartView.setCompanionTextView(
- (TextView) view.findViewById(R.id.companion_text));
+ final TextView companionTextView = (TextView) view.findViewById(R.id.companion_text);
+ mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart);
+ mDailyChartView.setCompanionTextView(companionTextView);
+ mHourlyChartView = (BatteryChartView) view.findViewById(R.id.hourly_battery_chart);
+ mHourlyChartView.setCompanionTextView(companionTextView);
if (mChartPreferenceController != null) {
- mChartPreferenceController.setBatteryChartView(mBatteryChartView);
+ mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
} else {
final TextView chargeView = (TextView) view.findViewById(R.id.charge);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
new file mode 100644
index 00000000000..4ff9eeba9b2
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/** Wraps the battery timestamp and level data used for battery usage chart. */
+public final class BatteryLevelData {
+ /** A container for the battery timestamp and level data. */
+ public static final class PeriodBatteryLevelData {
+ // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
+ // there is no level data for the corresponding timestamp.
+ private final List mTimestamps;
+ private final List mLevels;
+
+ public PeriodBatteryLevelData(
+ @NonNull List timestamps, @NonNull List levels) {
+ Preconditions.checkArgument(timestamps.size() == levels.size(),
+ /* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
+ + levels.size());
+ mTimestamps = timestamps;
+ mLevels = levels;
+ }
+
+ public List getTimestamps() {
+ return mTimestamps;
+ }
+
+ public List getLevels() {
+ return mLevels;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s",
+ Objects.toString(mTimestamps), Objects.toString(mLevels));
+ }
+ }
+
+ /**
+ * There could be 2 cases for the daily battery levels:
+ * 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
+ * data of 2022-01-01 06:00 and 2022-01-01 16:00.
+ * 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
+ * data of every day between the start and end, such as data of 2022-01-01 06:00,
+ * 2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
+ */
+ private final PeriodBatteryLevelData mDailyBatteryLevels;
+ // The size of hourly data must be the size of daily data - 1.
+ private final List mHourlyBatteryLevelsPerDay;
+
+ public BatteryLevelData(
+ @NonNull PeriodBatteryLevelData dailyBatteryLevels,
+ @NonNull List hourlyBatteryLevelsPerDay) {
+ final long dailySize = dailyBatteryLevels.getTimestamps().size();
+ final long hourlySize = hourlyBatteryLevelsPerDay.size();
+ Preconditions.checkArgument(hourlySize == dailySize - 1,
+ /* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
+ mDailyBatteryLevels = dailyBatteryLevels;
+ mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
+ }
+
+ public PeriodBatteryLevelData getDailyBatteryLevels() {
+ return mDailyBatteryLevels;
+ }
+
+ public List getHourlyBatteryLevelsPerDay() {
+ return mHourlyBatteryLevelsPerDay;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ENGLISH,
+ "dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s",
+ Objects.toString(mDailyBatteryLevels),
+ Objects.toString(mHourlyBatteryLevelsPerDay));
+ }
+}
+
diff --git a/src/com/android/settings/fuelgauge/BatteryUsageStatsLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageStatsLoader.java
similarity index 95%
rename from src/com/android/settings/fuelgauge/BatteryUsageStatsLoader.java
rename to src/com/android/settings/fuelgauge/batteryusage/BatteryUsageStatsLoader.java
index 65f2ddbb5be..d61b2764633 100644
--- a/src/com/android/settings/fuelgauge/BatteryUsageStatsLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageStatsLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
import android.os.BatteryStatsManager;
diff --git a/src/com/android/settings/fuelgauge/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
similarity index 61%
rename from src/com/android/settings/fuelgauge/ConvertUtils.java
rename to src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index 696147b44d4..130357378f1 100644
--- a/src/com/android/settings/fuelgauge/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -1,17 +1,19 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
-package com.android.settings.fuelgauge;
+package com.android.settings.fuelgauge.batteryusage;
import android.annotation.IntDef;
import android.content.ContentValues;
@@ -19,6 +21,7 @@ import android.content.Context;
import android.os.BatteryUsageStats;
import android.os.LocaleList;
import android.os.UserHandle;
+import android.os.UserManager;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.ArraySet;
@@ -26,6 +29,8 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.Utils;
+import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.overlay.FeatureFactory;
import java.lang.annotation.Retention;
@@ -46,15 +51,10 @@ public final class ConvertUtils {
private static final String TAG = "ConvertUtils";
private static final Map EMPTY_BATTERY_MAP = new HashMap<>();
private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
- new BatteryHistEntry(new ContentValues());
+ new BatteryHistEntry(new ContentValues());
// Maximum total time value for each slot cumulative data at most 2 hours.
private static final float TOTAL_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
- // Keys for metric metadata.
- static final int METRIC_KEY_PACKAGE = 1;
- static final int METRIC_KEY_BATTERY_LEVEL = 2;
- static final int METRIC_KEY_BATTERY_USAGE = 3;
-
@VisibleForTesting
static double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
@@ -64,22 +64,25 @@ public final class ConvertUtils {
public static final String FAKE_PACKAGE_NAME = "fake_package";
@IntDef(prefix = {"CONSUMER_TYPE"}, value = {
- CONSUMER_TYPE_UNKNOWN,
- CONSUMER_TYPE_UID_BATTERY,
- CONSUMER_TYPE_USER_BATTERY,
- CONSUMER_TYPE_SYSTEM_BATTERY,
+ CONSUMER_TYPE_UNKNOWN,
+ CONSUMER_TYPE_UID_BATTERY,
+ CONSUMER_TYPE_USER_BATTERY,
+ CONSUMER_TYPE_SYSTEM_BATTERY,
})
@Retention(RetentionPolicy.SOURCE)
- public static @interface ConsumerType {}
+ public static @interface ConsumerType {
+ }
public static final int CONSUMER_TYPE_UNKNOWN = 0;
public static final int CONSUMER_TYPE_UID_BATTERY = 1;
public static final int CONSUMER_TYPE_USER_BATTERY = 2;
public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;
- private ConvertUtils() {}
+ private ConvertUtils() {
+ }
- public static ContentValues convert(
+ /** Converts to content values */
+ public static ContentValues convertToContentValues(
BatteryEntry entry,
BatteryUsageStats batteryUsageStats,
int batteryLevel,
@@ -91,25 +94,25 @@ public final class ConvertUtils {
if (entry != null && batteryUsageStats != null) {
values.put(BatteryHistEntry.KEY_UID, Long.valueOf(entry.getUid()));
values.put(BatteryHistEntry.KEY_USER_ID,
- Long.valueOf(UserHandle.getUserId(entry.getUid())));
+ Long.valueOf(UserHandle.getUserId(entry.getUid())));
values.put(BatteryHistEntry.KEY_APP_LABEL, entry.getLabel());
values.put(BatteryHistEntry.KEY_PACKAGE_NAME,
- entry.getDefaultPackageName());
+ entry.getDefaultPackageName());
values.put(BatteryHistEntry.KEY_IS_HIDDEN, Boolean.valueOf(entry.isHidden()));
values.put(BatteryHistEntry.KEY_TOTAL_POWER,
- Double.valueOf(batteryUsageStats.getConsumedPower()));
+ Double.valueOf(batteryUsageStats.getConsumedPower()));
values.put(BatteryHistEntry.KEY_CONSUME_POWER,
- Double.valueOf(entry.getConsumedPower()));
+ Double.valueOf(entry.getConsumedPower()));
values.put(BatteryHistEntry.KEY_PERCENT_OF_TOTAL,
- Double.valueOf(entry.mPercent));
+ Double.valueOf(entry.mPercent));
values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME,
- Long.valueOf(entry.getTimeInForegroundMs()));
+ Long.valueOf(entry.getTimeInForegroundMs()));
values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME,
- Long.valueOf(entry.getTimeInBackgroundMs()));
+ Long.valueOf(entry.getTimeInBackgroundMs()));
values.put(BatteryHistEntry.KEY_DRAIN_TYPE,
- Integer.valueOf(entry.getPowerComponentId()));
+ Integer.valueOf(entry.getPowerComponentId()));
values.put(BatteryHistEntry.KEY_CONSUMER_TYPE,
- Integer.valueOf(entry.getConsumerType()));
+ Integer.valueOf(entry.getConsumerType()));
} else {
values.put(BatteryHistEntry.KEY_PACKAGE_NAME, FAKE_PACKAGE_NAME);
}
@@ -122,23 +125,47 @@ public final class ConvertUtils {
return values;
}
+ /** Converts to {@link BatteryHistEntry} */
+ public static BatteryHistEntry convertToBatteryHistEntry(
+ BatteryEntry entry,
+ BatteryUsageStats batteryUsageStats) {
+ return new BatteryHistEntry(
+ convertToContentValues(
+ entry,
+ batteryUsageStats,
+ /*batteryLevel=*/ 0,
+ /*batteryStatus=*/ 0,
+ /*batteryHealth=*/ 0,
+ /*bootTimestamp=*/ 0,
+ /*timestamp=*/ 0));
+ }
+
/** Converts UTC timestamp to human readable local time string. */
public static String utcToLocalTime(Context context, long timestamp) {
final Locale locale = getLocale(context);
final String pattern =
- DateFormat.getBestDateTimePattern(locale, "MMM dd,yyyy HH:mm:ss");
+ DateFormat.getBestDateTimePattern(locale, "MMM dd,yyyy HH:mm:ss");
return DateFormat.format(pattern, timestamp).toString();
}
/** Converts UTC timestamp to local time hour data. */
public static String utcToLocalTimeHour(
- Context context, long timestamp, boolean is24HourFormat) {
+ final Context context, final long timestamp, final boolean is24HourFormat) {
final Locale locale = getLocale(context);
- // e.g. for 12-hour format: 9 pm
+ // e.g. for 12-hour format: 9 PM
// e.g. for 24-hour format: 09:00
final String skeleton = is24HourFormat ? "HHm" : "ha";
final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
- return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale);
+ return DateFormat.format(pattern, timestamp).toString();
+ }
+
+ /** Converts UTC timestamp to local time day of week data. */
+ public static String utcToLocalTimeDayOfWeek(
+ final Context context, final long timestamp, final boolean isAbbreviation) {
+ final Locale locale = getLocale(context);
+ final String pattern = DateFormat.getBestDateTimePattern(locale,
+ isAbbreviation ? "E" : "EEEE");
+ return DateFormat.format(pattern, timestamp).toString();
}
/** Gets indexed battery usage data for each corresponding time slot. */
@@ -159,18 +186,18 @@ public final class ConvertUtils {
final int timestampStride = 2;
for (int index = 0; index < timeSlotSize; index++) {
final Long currentTimestamp =
- Long.valueOf(batteryHistoryKeys[index * timestampStride]);
+ Long.valueOf(batteryHistoryKeys[index * timestampStride]);
final Long nextTimestamp =
- Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]);
+ Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]);
final Long nextTwoTimestamp =
- Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]);
+ Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]);
// Fetches BatteryHistEntry data from corresponding time slot.
final Map currentBatteryHistMap =
- batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
+ batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
final Map nextBatteryHistMap =
- batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
+ batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
final Map nextTwoBatteryHistMap =
- batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
+ batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
// We should not get the empty list since we have at least one fake data to record
// the battery level and status in each time slot, the empty list is used to
// represent there is no enough data to apply interpolation arithmetic.
@@ -195,27 +222,27 @@ public final class ConvertUtils {
// Calculates all packages diff usage data in a specific time slot.
for (String key : allBatteryHistEntryKeys) {
final BatteryHistEntry currentEntry =
- currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
final BatteryHistEntry nextEntry =
- nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
final BatteryHistEntry nextTwoEntry =
- nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
// Cumulative values is a specific time slot for a specific app.
long foregroundUsageTimeInMs =
- getDiffValue(
- currentEntry.mForegroundUsageTimeInMs,
- nextEntry.mForegroundUsageTimeInMs,
- nextTwoEntry.mForegroundUsageTimeInMs);
+ getDiffValue(
+ currentEntry.mForegroundUsageTimeInMs,
+ nextEntry.mForegroundUsageTimeInMs,
+ nextTwoEntry.mForegroundUsageTimeInMs);
long backgroundUsageTimeInMs =
- getDiffValue(
- currentEntry.mBackgroundUsageTimeInMs,
- nextEntry.mBackgroundUsageTimeInMs,
- nextTwoEntry.mBackgroundUsageTimeInMs);
+ getDiffValue(
+ currentEntry.mBackgroundUsageTimeInMs,
+ nextEntry.mBackgroundUsageTimeInMs,
+ nextTwoEntry.mBackgroundUsageTimeInMs);
double consumePower =
- getDiffValue(
- currentEntry.mConsumePower,
- nextEntry.mConsumePower,
- nextTwoEntry.mConsumePower);
+ getDiffValue(
+ currentEntry.mConsumePower,
+ nextEntry.mConsumePower,
+ nextTwoEntry.mConsumePower);
// Excludes entry since we don't have enough data to calculate.
if (foregroundUsageTimeInMs == 0
&& backgroundUsageTimeInMs == 0
@@ -223,14 +250,14 @@ public final class ConvertUtils {
continue;
}
final BatteryHistEntry selectedBatteryEntry =
- selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
+ selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
if (selectedBatteryEntry == null) {
continue;
}
// Forces refine the cumulative value since it may introduce deviation
// error since we will apply the interpolation arithmetic.
final float totalUsageTimeInMs =
- foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ foregroundUsageTimeInMs + backgroundUsageTimeInMs;
if (totalUsageTimeInMs > TOTAL_TIME_THRESHOLD) {
final float ratio = TOTAL_TIME_THRESHOLD / totalUsageTimeInMs;
if (DEBUG) {
@@ -240,37 +267,75 @@ public final class ConvertUtils {
currentEntry));
}
foregroundUsageTimeInMs =
- Math.round(foregroundUsageTimeInMs * ratio);
+ Math.round(foregroundUsageTimeInMs * ratio);
backgroundUsageTimeInMs =
- Math.round(backgroundUsageTimeInMs * ratio);
+ Math.round(backgroundUsageTimeInMs * ratio);
consumePower = consumePower * ratio;
}
totalConsumePower += consumePower;
batteryDiffEntryList.add(
- new BatteryDiffEntry(
- context,
- foregroundUsageTimeInMs,
- backgroundUsageTimeInMs,
- consumePower,
- selectedBatteryEntry));
+ new BatteryDiffEntry(
+ context,
+ foregroundUsageTimeInMs,
+ backgroundUsageTimeInMs,
+ consumePower,
+ selectedBatteryEntry));
}
// Sets total consume power data into all BatteryDiffEntry in the same slot.
for (BatteryDiffEntry diffEntry : batteryDiffEntryList) {
diffEntry.setTotalConsumePower(totalConsumePower);
}
}
- insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
+ insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap);
+ resolveMultiUsersData(context, resultMap);
if (purgeLowPercentageAndFakeData) {
purgeLowPercentageAndFakeData(context, resultMap);
}
return resultMap;
}
+ @VisibleForTesting
+ static void resolveMultiUsersData(
+ final Context context,
+ final Map> indexedUsageMap) {
+ final int currentUserId = context.getUserId();
+ final UserHandle userHandle =
+ Utils.getManagedProfile(context.getSystemService(UserManager.class));
+ final int workProfileUserId =
+ userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
+ // Loops for all BatteryDiffEntry in the different slots.
+ for (List entryList : indexedUsageMap.values()) {
+ double consumePowerFromOtherUsers = 0f;
+ double consumePercentageFromOtherUsers = 0f;
+ final Iterator iterator = entryList.iterator();
+ while (iterator.hasNext()) {
+ final BatteryDiffEntry entry = iterator.next();
+ final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
+ if (batteryHistEntry.mConsumerType != CONSUMER_TYPE_UID_BATTERY) {
+ continue;
+ }
+ // Whether the BatteryHistEntry represents the current user data?
+ if (batteryHistEntry.mUserId == currentUserId
+ || batteryHistEntry.mUserId == workProfileUserId) {
+ continue;
+ }
+ // Removes and aggregates non-current users data from the list.
+ iterator.remove();
+ consumePowerFromOtherUsers += entry.mConsumePower;
+ consumePercentageFromOtherUsers += entry.getPercentOfTotal();
+ }
+ if (consumePercentageFromOtherUsers != 0) {
+ entryList.add(createOtherUsersEntry(context, consumePowerFromOtherUsers,
+ consumePercentageFromOtherUsers));
+ }
+ }
+ }
+
private static void insert24HoursData(
final int desiredIndex,
final Map> indexedUsageMap) {
final Map resultMap = new HashMap<>();
- double totalConsumePower = 0.0;
+ double totalConsumePower = 0f;
// Loops for all BatteryDiffEntry and aggregate them together.
for (List entryList : indexedUsageMap.values()) {
for (BatteryDiffEntry entry : entryList) {
@@ -282,9 +347,9 @@ public final class ConvertUtils {
} else {
// Sums up some fields data into the existing one.
oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
- entry.mForegroundUsageTimeInMs;
+ entry.mForegroundUsageTimeInMs;
oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
- entry.mBackgroundUsageTimeInMs;
+ entry.mBackgroundUsageTimeInMs;
oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
}
totalConsumePower += entry.mConsumePower;
@@ -318,7 +383,7 @@ public final class ConvertUtils {
if (packageName != null
&& !backgroundUsageTimeHideList.isEmpty()
&& backgroundUsageTimeHideList.contains(packageName)) {
- entry.mBackgroundUsageTimeInMs = 0;
+ entry.mBackgroundUsageTimeInMs = 0;
}
}
}
@@ -342,7 +407,7 @@ public final class ConvertUtils {
return entry2;
} else {
return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY
- ? entry3 : null;
+ ? entry3 : null;
}
}
@@ -352,8 +417,26 @@ public final class ConvertUtils {
return Locale.getDefault();
}
final LocaleList locales =
- context.getResources().getConfiguration().getLocales();
+ context.getResources().getConfiguration().getLocales();
return locales != null && !locales.isEmpty() ? locales.get(0)
- : Locale.getDefault();
+ : Locale.getDefault();
+ }
+
+ private static BatteryDiffEntry createOtherUsersEntry(
+ Context context, double consumePower, double consumePercentage) {
+ final ContentValues values = new ContentValues();
+ values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS);
+ values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS);
+ values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, CONSUMER_TYPE_UID_BATTERY);
+ // We will show the percentage for the "other users" item only, the aggregated
+ // running time information is useless for users to identify individual apps.
+ final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry(
+ context,
+ /*foregroundUsageTimeInMs=*/ 0,
+ /*backgroundUsageTimeInMs=*/ 0,
+ consumePower,
+ new BatteryHistEntry(values));
+ batteryDiffEntry.setTotalConsumePower(100 * consumePower / consumePercentage);
+ return batteryDiffEntry;
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
new file mode 100644
index 00000000000..b9c3a6ff7f3
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -0,0 +1,1292 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime;
+
+import android.app.settings.SettingsEnums;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.Utils;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.fuelgauge.BatteryStatus;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A utility class to process data loaded from database and make the data easy to use for battery
+ * usage UI.
+ */
+public final class DataProcessor {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "DataProcessor";
+ private static final int MIN_DAILY_DATA_SIZE = 2;
+ private static final int MIN_TIMESTAMP_DATA_SIZE = 2;
+ private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5;
+ // Maximum total time value for each hourly slot cumulative data at most 2 hours.
+ private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
+ private static final Map EMPTY_BATTERY_MAP = new HashMap<>();
+ private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
+ new BatteryHistEntry(new ContentValues());
+
+ @VisibleForTesting
+ static final double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
+ @VisibleForTesting
+ static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL;
+
+ /** A fake package name to represent no BatteryEntry data. */
+ public static final String FAKE_PACKAGE_NAME = "fake_package";
+
+ /** A callback listener when battery usage loading async task is executed. */
+ public interface UsageMapAsyncResponse {
+ /** The callback function when batteryUsageMap is loaded. */
+ void onBatteryUsageMapLoaded(
+ Map> batteryUsageMap);
+ }
+
+ private DataProcessor() {
+ }
+
+ /**
+ * @return Returns battery level data and start async task to compute battery diff usage data
+ * and load app labels + icons.
+ * Returns null if the input is invalid or not having at least 2 hours data.
+ */
+ @Nullable
+ public static BatteryLevelData getBatteryLevelData(
+ Context context,
+ @Nullable Handler handler,
+ @Nullable final Map> batteryHistoryMap,
+ final UsageMapAsyncResponse asyncResponseDelegate) {
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ Log.d(TAG, "batteryHistoryMap is null in getBatteryLevelData()");
+ loadBatteryUsageDataFromBatteryStatsService(
+ context, handler, asyncResponseDelegate);
+ return null;
+ }
+ handler = handler != null ? handler : new Handler(Looper.getMainLooper());
+ // Process raw history map data into hourly timestamps.
+ final Map> processedBatteryHistoryMap =
+ getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
+ // Wrap and processed history map into easy-to-use format for UI rendering.
+ final BatteryLevelData batteryLevelData =
+ getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
+ if (batteryLevelData == null) {
+ loadBatteryUsageDataFromBatteryStatsService(
+ context, handler, asyncResponseDelegate);
+ Log.d(TAG, "getBatteryLevelData() returns null");
+ return null;
+ }
+
+ // Start the async task to compute diff usage data and load labels and icons.
+ new ComputeUsageMapAndLoadItemsTask(
+ context,
+ handler,
+ asyncResponseDelegate,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(),
+ processedBatteryHistoryMap).execute();
+
+ return batteryLevelData;
+ }
+
+ /**
+ * @return Returns battery usage data of different entries.
+ * Returns null if the input is invalid or there is no enough data.
+ */
+ @Nullable
+ public static Map> getBatteryUsageData(
+ Context context,
+ @Nullable final Map> batteryHistoryMap) {
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ Log.d(TAG, "getBatteryLevelData() returns null");
+ return null;
+ }
+ // Process raw history map data into hourly timestamps.
+ final Map> processedBatteryHistoryMap =
+ getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
+ // Wrap and processed history map into easy-to-use format for UI rendering.
+ final BatteryLevelData batteryLevelData =
+ getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
+ return batteryLevelData == null
+ ? null
+ : getBatteryUsageMap(
+ context,
+ batteryLevelData.getHourlyBatteryLevelsPerDay(),
+ processedBatteryHistoryMap);
+ }
+
+ /**
+ * @return Returns whether the target is in the CharSequence array.
+ */
+ public static boolean contains(String target, CharSequence[] packageNames) {
+ if (target != null && packageNames != null) {
+ for (CharSequence packageName : packageNames) {
+ if (TextUtils.equals(target, packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return Returns the processed history map which has interpolated to every hour data.
+ * The start and end timestamp must be the even hours.
+ * The keys of processed history map should contain every hour between the start and end
+ * timestamp. If there's no data in some key, the value will be the empty hashmap.
+ */
+ @VisibleForTesting
+ static Map> getHistoryMapWithExpectedTimestamps(
+ Context context,
+ final Map> batteryHistoryMap) {
+ final long startTime = System.currentTimeMillis();
+ final List rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet());
+ final Map> resultMap = new HashMap();
+ if (rawTimestampList.isEmpty()) {
+ Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()");
+ return resultMap;
+ }
+ Collections.sort(rawTimestampList);
+ final List expectedTimestampList = getTimestampSlots(rawTimestampList);
+ final boolean isFromFullCharge =
+ isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0)));
+ interpolateHistory(
+ context, rawTimestampList, expectedTimestampList, isFromFullCharge,
+ batteryHistoryMap, resultMap);
+ Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms",
+ resultMap.size(), (System.currentTimeMillis() - startTime)));
+ return resultMap;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static BatteryLevelData getLevelDataThroughProcessedHistoryMap(
+ Context context,
+ final Map> processedBatteryHistoryMap) {
+ final List timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet());
+ Collections.sort(timestampList);
+ final List dailyTimestamps = getDailyTimestamps(timestampList);
+ // There should be at least the start and end timestamps. Otherwise, return null to not show
+ // data in usage chart.
+ if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+ return null;
+ }
+
+ final List> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
+ final BatteryLevelData.PeriodBatteryLevelData dailyLevelData =
+ getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps);
+ final List hourlyLevelData =
+ getHourlyPeriodBatteryLevelData(
+ context, processedBatteryHistoryMap, hourlyTimestamps);
+ return new BatteryLevelData(dailyLevelData, hourlyLevelData);
+ }
+
+ /**
+ * Computes expected timestamp slots for last full charge, which will return hourly timestamps
+ * between start and end two even hour values.
+ */
+ @VisibleForTesting
+ static List getTimestampSlots(final List rawTimestampList) {
+ final List timestampSlots = new ArrayList<>();
+ final int rawTimestampListSize = rawTimestampList.size();
+ // If timestamp number is smaller than 2, the following computation is not necessary.
+ if (rawTimestampListSize < MIN_TIMESTAMP_DATA_SIZE) {
+ return timestampSlots;
+ }
+ final long rawStartTimestamp = rawTimestampList.get(0);
+ final long rawEndTimestamp = rawTimestampList.get(rawTimestampListSize - 1);
+ // No matter the start is from last full charge or 6 days ago, use the nearest even hour.
+ final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp);
+ // Use the even hour before the raw end timestamp as the end.
+ final long endTimestamp = getLastEvenHourBeforeTimestamp(rawEndTimestamp);
+ // If the start timestamp is later or equal the end one, return the empty list.
+ if (startTimestamp >= endTimestamp) {
+ return timestampSlots;
+ }
+ for (long timestamp = startTimestamp; timestamp <= endTimestamp;
+ timestamp += DateUtils.HOUR_IN_MILLIS) {
+ timestampSlots.add(timestamp);
+ }
+ return timestampSlots;
+ }
+
+ /**
+ * Computes expected daily timestamp slots.
+ *
+ * The valid result should be composed of 3 parts:
+ * 1) start timestamp
+ * 2) every 00:00 timestamp (default timezone) between the start and end
+ * 3) end timestamp
+ * Otherwise, returns an empty list.
+ */
+ @VisibleForTesting
+ static List getDailyTimestamps(final List timestampList) {
+ final List dailyTimestampList = new ArrayList<>();
+ // If timestamp number is smaller than 2, the following computation is not necessary.
+ if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
+ return dailyTimestampList;
+ }
+ final long startTime = timestampList.get(0);
+ final long endTime = timestampList.get(timestampList.size() - 1);
+ long nextDay = getTimestampOfNextDay(startTime);
+ dailyTimestampList.add(startTime);
+ while (nextDay < endTime) {
+ dailyTimestampList.add(nextDay);
+ nextDay += DateUtils.DAY_IN_MILLIS;
+ }
+ dailyTimestampList.add(endTime);
+ return dailyTimestampList;
+ }
+
+ @VisibleForTesting
+ static boolean isFromFullCharge(@Nullable final Map entryList) {
+ if (entryList == null) {
+ Log.d(TAG, "entryList is null in isFromFullCharge()");
+ return false;
+ }
+ final List entryKeys = new ArrayList<>(entryList.keySet());
+ if (entryKeys.isEmpty()) {
+ Log.d(TAG, "empty entryList in isFromFullCharge()");
+ return false;
+ }
+ // The hist entries in the same timestamp should have same battery status and level.
+ // Checking the first one should be enough.
+ final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0));
+ return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel);
+ }
+
+ @VisibleForTesting
+ static long[] findNearestTimestamp(final List timestamps, final long target) {
+ final long[] results = new long[] {Long.MIN_VALUE, Long.MAX_VALUE};
+ // Searches the nearest lower and upper timestamp value.
+ timestamps.forEach(timestamp -> {
+ if (timestamp <= target && timestamp > results[0]) {
+ results[0] = timestamp;
+ }
+ if (timestamp >= target && timestamp < results[1]) {
+ results[1] = timestamp;
+ }
+ });
+ // Uses zero value to represent invalid searching result.
+ results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0];
+ results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1];
+ return results;
+ }
+
+ /**
+ * @return Returns the timestamp for 00:00 1 day after the given timestamp based on local
+ * timezone.
+ */
+ @VisibleForTesting
+ static long getTimestampOfNextDay(long timestamp) {
+ return getTimestampWithDayDiff(timestamp, /*dayDiff=*/ 1);
+ }
+
+ /**
+ * Returns whether currentSlot will be used in daily chart.
+ */
+ @VisibleForTesting
+ static boolean isForDailyChart(final boolean isStartOrEnd, final long currentSlot) {
+ // The start and end timestamps will always be used in daily chart.
+ if (isStartOrEnd) {
+ return true;
+ }
+
+ // The timestamps for 00:00 will be used in daily chart.
+ final long startOfTheDay = getTimestampWithDayDiff(currentSlot, /*dayDiff=*/ 0);
+ return currentSlot == startOfTheDay;
+ }
+
+ /**
+ * @return Returns the indexed battery usage data for each corresponding time slot.
+ *
+ * There could be 2 cases of the returned value:
+ * 1) null: empty or invalid data.
+ * 2) non-null: must be a 2d map and composed by 3 parts:
+ * 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
+ * 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]
+ * 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]
+ */
+ @VisibleForTesting
+ @Nullable
+ static Map> getBatteryUsageMap(
+ final Context context,
+ final List hourlyBatteryLevelsPerDay,
+ final Map> batteryHistoryMap) {
+ if (batteryHistoryMap.isEmpty()) {
+ return null;
+ }
+ final Map> resultMap = new HashMap<>();
+ // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex].
+ insertHourlyUsageDiffData(
+ context, hourlyBatteryLevelsPerDay, batteryHistoryMap, resultMap);
+ // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL].
+ insertDailyUsageDiffData(hourlyBatteryLevelsPerDay, resultMap);
+ // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL].
+ insertAllUsageDiffData(resultMap);
+ // Compute the apps number before purge. Must put before purgeLowPercentageAndFakeData.
+ final int countOfAppBeforePurge = getCountOfApps(resultMap);
+ purgeLowPercentageAndFakeData(context, resultMap);
+ // Compute the apps number after purge. Must put after purgeLowPercentageAndFakeData.
+ final int countOfAppAfterPurge = getCountOfApps(resultMap);
+ if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) {
+ return null;
+ }
+
+ logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge);
+ return resultMap;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static BatteryDiffData generateBatteryDiffData(
+ final Context context,
+ @Nullable final List batteryEntryList,
+ final BatteryUsageStats batteryUsageStats) {
+ final List batteryHistEntryList =
+ convertToBatteryHistEntry(batteryEntryList, batteryUsageStats);
+ if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) {
+ Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()");
+ return null;
+ }
+ final int currentUserId = context.getUserId();
+ final UserHandle userHandle =
+ Utils.getManagedProfile(context.getSystemService(UserManager.class));
+ final int workProfileUserId =
+ userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
+ final List appEntries = new ArrayList<>();
+ final List systemEntries = new ArrayList<>();
+ double totalConsumePower = 0f;
+ double consumePowerFromOtherUsers = 0f;
+
+ for (BatteryHistEntry entry : batteryHistEntryList) {
+ final boolean isFromOtherUsers = isConsumedFromOtherUsers(
+ currentUserId, workProfileUserId, entry);
+ totalConsumePower += entry.mConsumePower;
+ if (isFromOtherUsers) {
+ consumePowerFromOtherUsers += entry.mConsumePower;
+ } else {
+ final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
+ context,
+ entry.mForegroundUsageTimeInMs,
+ entry.mBackgroundUsageTimeInMs,
+ entry.mConsumePower,
+ entry);
+ if (currentBatteryDiffEntry.isSystemEntry()) {
+ systemEntries.add(currentBatteryDiffEntry);
+ } else {
+ appEntries.add(currentBatteryDiffEntry);
+ }
+ }
+ }
+ if (consumePowerFromOtherUsers != 0) {
+ systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers));
+ }
+
+ // If there is no data, return null instead of empty item.
+ if (appEntries.isEmpty() && systemEntries.isEmpty()) {
+ return null;
+ }
+
+ return new BatteryDiffData(appEntries, systemEntries, totalConsumePower);
+ }
+
+ /**
+ * Starts the async task to load battery diff usage data and load app labels + icons.
+ */
+ private static void loadBatteryUsageDataFromBatteryStatsService(
+ Context context,
+ @Nullable Handler handler,
+ final UsageMapAsyncResponse asyncResponseDelegate) {
+ new LoadUsageMapFromBatteryStatsServiceTask(
+ context,
+ handler,
+ asyncResponseDelegate).execute();
+ }
+
+ /**
+ * @return Returns the overall battery usage data from battery stats service directly.
+ *
+ * The returned value should be always a 2d map and composed by only 1 part:
+ * - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
+ */
+ @Nullable
+ private static Map> getBatteryUsageMapFromStatsService(
+ final Context context) {
+ final Map> resultMap = new HashMap<>();
+ final Map allUsageMap = new HashMap<>();
+ // Always construct the map whether the value is null or not.
+ allUsageMap.put(SELECTED_INDEX_ALL,
+ getBatteryDiffDataFromBatteryStatsService(context));
+ resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
+
+ // Compute the apps number before purge. Must put before purgeLowPercentageAndFakeData.
+ final int countOfAppBeforePurge = getCountOfApps(resultMap);
+ purgeLowPercentageAndFakeData(context, resultMap);
+ // Compute the apps number after purge. Must put after purgeLowPercentageAndFakeData.
+ final int countOfAppAfterPurge = getCountOfApps(resultMap);
+
+ logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge);
+ return resultMap;
+ }
+
+ @Nullable
+ private static BatteryDiffData getBatteryDiffDataFromBatteryStatsService(
+ final Context context) {
+ BatteryDiffData batteryDiffData = null;
+ try {
+ final BatteryUsageStatsQuery batteryUsageStatsQuery =
+ new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build();
+ final BatteryUsageStats batteryUsageStats =
+ context.getSystemService(BatteryStatsManager.class)
+ .getBatteryUsageStats(batteryUsageStatsQuery);
+
+ if (batteryUsageStats == null) {
+ Log.w(TAG, "batteryUsageStats is null content");
+ return null;
+ }
+
+ final List batteryEntryList =
+ generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats);
+ batteryDiffData = generateBatteryDiffData(context, batteryEntryList, batteryUsageStats);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "load batteryUsageStats:" + e);
+ }
+
+ return batteryDiffData;
+ }
+
+ @Nullable
+ private static List generateBatteryEntryListFromBatteryUsageStats(
+ final Context context, final BatteryUsageStats batteryUsageStats) {
+ // Loads the battery consuming data.
+ final BatteryAppListPreferenceController controller =
+ new BatteryAppListPreferenceController(
+ context,
+ /*preferenceKey=*/ null,
+ /*lifecycle=*/ null,
+ /*activity*=*/ null,
+ /*fragment=*/ null);
+ return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true);
+ }
+
+ @Nullable
+ private static List convertToBatteryHistEntry(
+ @Nullable final List batteryEntryList,
+ final BatteryUsageStats batteryUsageStats) {
+ if (batteryEntryList == null || batteryEntryList.isEmpty()) {
+ Log.w(TAG, "batteryEntryList is null or empty in convertToBatteryHistEntry()");
+ return null;
+ }
+ return batteryEntryList.stream()
+ .filter(entry -> {
+ final long foregroundMs = entry.getTimeInForegroundMs();
+ final long backgroundMs = entry.getTimeInBackgroundMs();
+ return entry.getConsumedPower() > 0
+ || (entry.getConsumedPower() == 0
+ && (foregroundMs != 0 || backgroundMs != 0));
+ })
+ .map(entry -> ConvertUtils.convertToBatteryHistEntry(
+ entry,
+ batteryUsageStats))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Interpolates history map based on expected timestamp slots and processes the corner case when
+ * the expected start timestamp is earlier than what we have.
+ */
+ private static void interpolateHistory(
+ Context context,
+ final List rawTimestampList,
+ final List expectedTimestampSlots,
+ final boolean isFromFullCharge,
+ final Map> batteryHistoryMap,
+ final Map> resultMap) {
+ if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) {
+ return;
+ }
+ final long expectedStartTimestamp = expectedTimestampSlots.get(0);
+ final long rawStartTimestamp = rawTimestampList.get(0);
+ int startIndex = 0;
+ // If the expected start timestamp is full charge or earlier than what we have, use the
+ // first data of what we have directly. This should be OK because the expected start
+ // timestamp is the nearest even hour of the raw start timestamp, their time diff is no
+ // more than 1 hour.
+ if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) {
+ startIndex = 1;
+ resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp));
+ }
+ final int expectedTimestampSlotsSize = expectedTimestampSlots.size();
+ for (int index = startIndex; index < expectedTimestampSlotsSize; index++) {
+ final long currentSlot = expectedTimestampSlots.get(index);
+ final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1;
+ interpolateHistoryForSlot(
+ context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap,
+ isStartOrEnd);
+ }
+ }
+
+ private static void interpolateHistoryForSlot(
+ Context context,
+ final long currentSlot,
+ final List rawTimestampList,
+ final Map> batteryHistoryMap,
+ final Map> resultMap,
+ final boolean isStartOrEnd) {
+ final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot);
+ final long lowerTimestamp = nearestTimestamps[0];
+ final long upperTimestamp = nearestTimestamps[1];
+ // Case 1: upper timestamp is zero since scheduler is delayed!
+ if (upperTimestamp == 0) {
+ log(context, "job scheduler is delayed", currentSlot, null);
+ resultMap.put(currentSlot, new HashMap<>());
+ return;
+ }
+ // Case 2: upper timestamp is closed to the current timestamp.
+ if ((upperTimestamp - currentSlot)
+ < MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * DateUtils.SECOND_IN_MILLIS) {
+ log(context, "force align into the nearest slot", currentSlot, null);
+ resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp));
+ return;
+ }
+ // Case 3: lower timestamp is zero before starting to collect data.
+ if (lowerTimestamp == 0) {
+ log(context, "no lower timestamp slot data", currentSlot, null);
+ resultMap.put(currentSlot, new HashMap<>());
+ return;
+ }
+ interpolateHistoryForSlot(context,
+ currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap,
+ isStartOrEnd);
+ }
+
+ private static void interpolateHistoryForSlot(
+ Context context,
+ final long currentSlot,
+ final long lowerTimestamp,
+ final long upperTimestamp,
+ final Map> batteryHistoryMap,
+ final Map> resultMap,
+ final boolean isStartOrEnd) {
+ final Map lowerEntryDataMap =
+ batteryHistoryMap.get(lowerTimestamp);
+ final Map upperEntryDataMap =
+ batteryHistoryMap.get(upperTimestamp);
+ // Verifies whether the lower data is valid to use or not by checking boot time.
+ final BatteryHistEntry upperEntryDataFirstEntry =
+ upperEntryDataMap.values().stream().findFirst().get();
+ final long upperEntryDataBootTimestamp =
+ upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp;
+ // Lower data is captured before upper data corresponding device is booting.
+ // Skips the booting-specific logics and always does interpolation for daily chart level
+ // data.
+ if (lowerTimestamp < upperEntryDataBootTimestamp
+ && !isForDailyChart(isStartOrEnd, currentSlot)) {
+ // Provides an opportunity to force align the slot directly.
+ if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) {
+ log(context, "force align into the nearest slot", currentSlot, null);
+ resultMap.put(currentSlot, upperEntryDataMap);
+ } else {
+ log(context, "in the different booting section", currentSlot, null);
+ resultMap.put(currentSlot, new HashMap<>());
+ }
+ return;
+ }
+ log(context, "apply interpolation arithmetic", currentSlot, null);
+ final Map newHistEntryMap = new HashMap<>();
+ final double timestampLength = upperTimestamp - lowerTimestamp;
+ final double timestampDiff = currentSlot - lowerTimestamp;
+ // Applies interpolation arithmetic for each BatteryHistEntry.
+ for (String entryKey : upperEntryDataMap.keySet()) {
+ final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey);
+ final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey);
+ // Checks whether there is any abnormal battery reset conditions.
+ if (lowerEntry != null) {
+ final boolean invalidForegroundUsageTime =
+ lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs;
+ final boolean invalidBackgroundUsageTime =
+ lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs;
+ if (invalidForegroundUsageTime || invalidBackgroundUsageTime) {
+ newHistEntryMap.put(entryKey, upperEntry);
+ log(context, "abnormal reset condition is found", currentSlot, upperEntry);
+ continue;
+ }
+ }
+ final BatteryHistEntry newEntry =
+ BatteryHistEntry.interpolate(
+ currentSlot,
+ upperTimestamp,
+ /*ratio=*/ timestampDiff / timestampLength,
+ lowerEntry,
+ upperEntry);
+ newHistEntryMap.put(entryKey, newEntry);
+ if (lowerEntry == null) {
+ log(context, "cannot find lower entry data", currentSlot, upperEntry);
+ continue;
+ }
+ }
+ resultMap.put(currentSlot, newHistEntryMap);
+ }
+
+ /**
+ * @return Returns the nearest even hour timestamp of the given timestamp.
+ */
+ private static long getNearestEvenHourTimestamp(long rawTimestamp) {
+ // If raw hour is even, the nearest even hour should be the even hour before raw
+ // start. The hour doesn't need to change and just set the minutes and seconds to 0.
+ // Otherwise, the nearest even hour should be raw hour + 1.
+ // For example, the nearest hour of 14:30:50 should be 14:00:00. While the nearest
+ // hour of 15:30:50 should be 16:00:00.
+ return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ 1);
+ }
+
+ /**
+ * @return Returns the last even hour timestamp before the given timestamp.
+ */
+ private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) {
+ // If raw hour is even, the hour doesn't need to change as well.
+ // Otherwise, the even hour before raw end should be raw hour - 1.
+ // For example, the even hour before 14:30:50 should be 14:00:00. While the even
+ // hour before 15:30:50 should be 14:00:00.
+ return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1);
+ }
+
+ private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) {
+ final Calendar evenHourCalendar = Calendar.getInstance();
+ evenHourCalendar.setTimeInMillis(rawTimestamp);
+ // Before computing the evenHourCalendar, record raw hour based on local timezone.
+ final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY);
+ if (rawHour % 2 != 0) {
+ evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay);
+ }
+ evenHourCalendar.set(Calendar.MINUTE, 0);
+ evenHourCalendar.set(Calendar.SECOND, 0);
+ evenHourCalendar.set(Calendar.MILLISECOND, 0);
+ return evenHourCalendar.getTimeInMillis();
+ }
+
+ private static List> getHourlyTimestamps(final List dailyTimestamps) {
+ final List> hourlyTimestamps = new ArrayList<>();
+ if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+ return hourlyTimestamps;
+ }
+
+ for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1;
+ dailyStartIndex++) {
+ long currentTimestamp = dailyTimestamps.get(dailyStartIndex);
+ final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1);
+ final List hourlyTimestampsPerDay = new ArrayList<>();
+ while (currentTimestamp <= dailyEndTimestamp) {
+ hourlyTimestampsPerDay.add(currentTimestamp);
+ currentTimestamp += 2 * DateUtils.HOUR_IN_MILLIS;
+ }
+ hourlyTimestamps.add(hourlyTimestampsPerDay);
+ }
+ return hourlyTimestamps;
+ }
+
+ private static List getHourlyPeriodBatteryLevelData(
+ Context context,
+ final Map> processedBatteryHistoryMap,
+ final List> timestamps) {
+ final List levelData = new ArrayList<>();
+ timestamps.forEach(
+ timestampList -> levelData.add(
+ getPeriodBatteryLevelData(
+ context, processedBatteryHistoryMap, timestampList)));
+ return levelData;
+ }
+
+ private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData(
+ Context context,
+ final Map> processedBatteryHistoryMap,
+ final List timestamps) {
+ final List levels = new ArrayList<>();
+ timestamps.forEach(
+ timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp)));
+ return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels);
+ }
+
+ private static Integer getLevel(
+ Context context,
+ final Map> processedBatteryHistoryMap,
+ final long timestamp) {
+ final Map entryMap = processedBatteryHistoryMap.get(timestamp);
+ if (entryMap == null || entryMap.isEmpty()) {
+ Log.e(TAG, "abnormal entry list in the timestamp:"
+ + utcToLocalTime(context, timestamp));
+ return null;
+ }
+ // Averages the battery level in each time slot to avoid corner conditions.
+ float batteryLevelCounter = 0;
+ for (BatteryHistEntry entry : entryMap.values()) {
+ batteryLevelCounter += entry.mBatteryLevel;
+ }
+ return Math.round(batteryLevelCounter / entryMap.size());
+ }
+
+ private static void insertHourlyUsageDiffData(
+ Context context,
+ final List hourlyBatteryLevelsPerDay,
+ final Map> batteryHistoryMap,
+ final Map> resultMap) {
+ final int currentUserId = context.getUserId();
+ final UserHandle userHandle =
+ Utils.getManagedProfile(context.getSystemService(UserManager.class));
+ final int workProfileUserId =
+ userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
+ // Each time slot usage diff data =
+ // Math.abs(timestamp[i+2] data - timestamp[i+1] data) +
+ // Math.abs(timestamp[i+1] data - timestamp[i] data);
+ // since we want to aggregate every two hours data into a single time slot.
+ for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
+ final Map dailyDiffMap = new HashMap<>();
+ resultMap.put(dailyIndex, dailyDiffMap);
+ if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
+ continue;
+ }
+ final List timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
+ for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
+ final BatteryDiffData hourlyBatteryDiffData =
+ insertHourlyUsageDiffDataPerSlot(
+ context,
+ currentUserId,
+ workProfileUserId,
+ hourlyIndex,
+ timestamps,
+ batteryHistoryMap);
+ dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData);
+ }
+ }
+ }
+
+ private static void insertDailyUsageDiffData(
+ final List hourlyBatteryLevelsPerDay,
+ final Map> resultMap) {
+ for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) {
+ Map dailyUsageMap = resultMap.get(index);
+ if (dailyUsageMap == null) {
+ dailyUsageMap = new HashMap<>();
+ resultMap.put(index, dailyUsageMap);
+ }
+ dailyUsageMap.put(
+ SELECTED_INDEX_ALL,
+ getAccumulatedUsageDiffData(dailyUsageMap.values()));
+ }
+ }
+
+ private static void insertAllUsageDiffData(
+ final Map> resultMap) {
+ final List diffDataList = new ArrayList<>();
+ resultMap.keySet().forEach(
+ key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL)));
+ final Map allUsageMap = new HashMap<>();
+ allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(diffDataList));
+ resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
+ }
+
+ @Nullable
+ private static BatteryDiffData insertHourlyUsageDiffDataPerSlot(
+ Context context,
+ final int currentUserId,
+ final int workProfileUserId,
+ final int currentIndex,
+ final List timestamps,
+ final Map> batteryHistoryMap) {
+ final List appEntries = new ArrayList<>();
+ final List systemEntries = new ArrayList<>();
+
+ final Long currentTimestamp = timestamps.get(currentIndex);
+ final Long nextTimestamp = currentTimestamp + DateUtils.HOUR_IN_MILLIS;
+ final Long nextTwoTimestamp = nextTimestamp + DateUtils.HOUR_IN_MILLIS;
+ // Fetches BatteryHistEntry data from corresponding time slot.
+ final Map currentBatteryHistMap =
+ batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
+ final Map nextBatteryHistMap =
+ batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
+ final Map nextTwoBatteryHistMap =
+ batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
+ // We should not get the empty list since we have at least one fake data to record
+ // the battery level and status in each time slot, the empty list is used to
+ // represent there is no enough data to apply interpolation arithmetic.
+ if (currentBatteryHistMap.isEmpty()
+ || nextBatteryHistMap.isEmpty()
+ || nextTwoBatteryHistMap.isEmpty()) {
+ return null;
+ }
+
+ // Collects all keys in these three time slot records as all populations.
+ final Set allBatteryHistEntryKeys = new ArraySet<>();
+ allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet());
+ allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet());
+ allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet());
+
+ double totalConsumePower = 0.0;
+ double consumePowerFromOtherUsers = 0f;
+ // Calculates all packages diff usage data in a specific time slot.
+ for (String key : allBatteryHistEntryKeys) {
+ final BatteryHistEntry currentEntry =
+ currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ final BatteryHistEntry nextEntry =
+ nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ final BatteryHistEntry nextTwoEntry =
+ nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+ // Cumulative values is a specific time slot for a specific app.
+ long foregroundUsageTimeInMs =
+ getDiffValue(
+ currentEntry.mForegroundUsageTimeInMs,
+ nextEntry.mForegroundUsageTimeInMs,
+ nextTwoEntry.mForegroundUsageTimeInMs);
+ long backgroundUsageTimeInMs =
+ getDiffValue(
+ currentEntry.mBackgroundUsageTimeInMs,
+ nextEntry.mBackgroundUsageTimeInMs,
+ nextTwoEntry.mBackgroundUsageTimeInMs);
+ double consumePower =
+ getDiffValue(
+ currentEntry.mConsumePower,
+ nextEntry.mConsumePower,
+ nextTwoEntry.mConsumePower);
+ // Excludes entry since we don't have enough data to calculate.
+ if (foregroundUsageTimeInMs == 0
+ && backgroundUsageTimeInMs == 0
+ && consumePower == 0) {
+ continue;
+ }
+ final BatteryHistEntry selectedBatteryEntry =
+ selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
+ if (selectedBatteryEntry == null) {
+ continue;
+ }
+ // Forces refine the cumulative value since it may introduce deviation error since we
+ // will apply the interpolation arithmetic.
+ final float totalUsageTimeInMs =
+ foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ if (totalUsageTimeInMs > TOTAL_HOURLY_TIME_THRESHOLD) {
+ final float ratio = TOTAL_HOURLY_TIME_THRESHOLD / totalUsageTimeInMs;
+ if (DEBUG) {
+ Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s",
+ Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
+ Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
+ currentEntry));
+ }
+ foregroundUsageTimeInMs =
+ Math.round(foregroundUsageTimeInMs * ratio);
+ backgroundUsageTimeInMs =
+ Math.round(backgroundUsageTimeInMs * ratio);
+ consumePower = consumePower * ratio;
+ }
+ totalConsumePower += consumePower;
+
+ final boolean isFromOtherUsers = isConsumedFromOtherUsers(
+ currentUserId, workProfileUserId, selectedBatteryEntry);
+ if (isFromOtherUsers) {
+ consumePowerFromOtherUsers += consumePower;
+ } else {
+ final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
+ context,
+ foregroundUsageTimeInMs,
+ backgroundUsageTimeInMs,
+ consumePower,
+ selectedBatteryEntry);
+ if (currentBatteryDiffEntry.isSystemEntry()) {
+ systemEntries.add(currentBatteryDiffEntry);
+ } else {
+ appEntries.add(currentBatteryDiffEntry);
+ }
+ }
+ }
+ if (consumePowerFromOtherUsers != 0) {
+ systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers));
+ }
+
+ // If there is no data, return null instead of empty item.
+ if (appEntries.isEmpty() && systemEntries.isEmpty()) {
+ return null;
+ }
+
+ final BatteryDiffData resultDiffData =
+ new BatteryDiffData(appEntries, systemEntries, totalConsumePower);
+ return resultDiffData;
+ }
+
+ private static boolean isConsumedFromOtherUsers(
+ final int currentUserId,
+ final int workProfileUserId,
+ final BatteryHistEntry batteryHistEntry) {
+ return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+ && batteryHistEntry.mUserId != currentUserId
+ && batteryHistEntry.mUserId != workProfileUserId;
+ }
+
+ @Nullable
+ private static BatteryDiffData getAccumulatedUsageDiffData(
+ final Collection diffEntryListData) {
+ double totalConsumePower = 0f;
+ final Map diffEntryMap = new HashMap<>();
+ final List appEntries = new ArrayList<>();
+ final List systemEntries = new ArrayList<>();
+
+ for (BatteryDiffData diffEntryList : diffEntryListData) {
+ if (diffEntryList == null) {
+ continue;
+ }
+ for (BatteryDiffEntry entry : diffEntryList.getAppDiffEntryList()) {
+ computeUsageDiffDataPerEntry(entry, diffEntryMap);
+ totalConsumePower += entry.mConsumePower;
+ }
+ for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) {
+ computeUsageDiffDataPerEntry(entry, diffEntryMap);
+ totalConsumePower += entry.mConsumePower;
+ }
+ }
+
+ final Collection diffEntryList = diffEntryMap.values();
+ for (BatteryDiffEntry entry : diffEntryList) {
+ // Sets total daily consume power data into all BatteryDiffEntry.
+ entry.setTotalConsumePower(totalConsumePower);
+ if (entry.isSystemEntry()) {
+ systemEntries.add(entry);
+ } else {
+ appEntries.add(entry);
+ }
+ }
+
+ return diffEntryList.isEmpty() ? null : new BatteryDiffData(appEntries, systemEntries);
+ }
+
+ private static void computeUsageDiffDataPerEntry(
+ final BatteryDiffEntry entry,
+ final Map diffEntryMap) {
+ final String key = entry.mBatteryHistEntry.getKey();
+ final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key);
+ // Creates new BatteryDiffEntry if we don't have it.
+ if (oldBatteryDiffEntry == null) {
+ diffEntryMap.put(key, entry.clone());
+ } else {
+ // Sums up some field data into the existing one.
+ oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
+ entry.mForegroundUsageTimeInMs;
+ oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
+ entry.mBackgroundUsageTimeInMs;
+ oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
+ }
+ }
+
+ // Removes low percentage data and fake usage data, which will be zero value.
+ private static void purgeLowPercentageAndFakeData(
+ final Context context,
+ final Map> resultMap) {
+ final Set backgroundUsageTimeHideList =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getHideBackgroundUsageTimeSet(context);
+ final CharSequence[] notAllowShowEntryPackages =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getHideApplicationEntries(context);
+ resultMap.keySet().forEach(dailyKey -> {
+ final Map