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 +280,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 +291,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 +311,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 9c18a0da608c297e864dee5b9679d9f734cc170e..e2d8c7edddc9b2c24f132c3bcb4bf5923840d649 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 426395c24fe7bed233b0706332222e3fd739336f..8d049836b6b4e4be6397835441fd84cd92a86664 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) {
+ 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 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.
- mInternal.setEnabled(false);
+
+ 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 onNavigateBack(View view) {
- finish();
+ 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 813bcc6e2018420001e83717b1598c76d0a3994d..8de94724afde06da96fadd93766fe145e3d392ad 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 4b87e422c1b81a321dbe2b5e772b3915fe5191e7..289db520ec0eb86e2ec3320b3de8da93bdbe750f 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 f8816fe13a88efa62655de54290c10120becc7c6..e6ac6069d9ab8ad1fa131111b49d3ca2d623acf7 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/deviceinfo/imei/ImeiInfoPreferenceController.java b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java
index 027e8c806ca5d13dfa97c14e616364db6c344953..d6f8a513e28c670fb6d8558890cae8ea01c7ae30 100644
--- a/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java
@@ -33,6 +33,7 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.deviceinfo.PhoneNumberSummaryPreference;
+import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.Utils;
import java.util.ArrayList;
@@ -63,6 +64,9 @@ public class ImeiInfoPreferenceController extends BasePreferenceController {
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
+ if (!SubscriptionUtil.isSimHardwareVisible(mContext)) {
+ return;
+ }
final Preference preference = screen.findPreference(getPreferenceKey());
final PreferenceCategory category = screen.findPreference(KEY_PREFERENCE_CATEGORY);
@@ -118,7 +122,8 @@ public class ImeiInfoPreferenceController extends BasePreferenceController {
@Override
public int getAvailabilityStatus() {
- return mContext.getSystemService(UserManager.class).isAdminUser()
+ return SubscriptionUtil.isSimHardwareVisible(mContext) &&
+ mContext.getSystemService(UserManager.class).isAdminUser()
&& !Utils.isWifiOnly(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java
index 4204ec14f9e64954a00cad154db233311615833c..96f8b62d23fcfcfe6b75d936bb2e1dcf2560d2fd 100644
--- a/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java
@@ -29,6 +29,7 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.deviceinfo.AbstractSimStatusImeiInfoPreferenceController;
import java.util.ArrayList;
@@ -59,9 +60,18 @@ public class SimStatusPreferenceController extends
return KEY_SIM_STATUS;
}
+ @Override
+ public boolean isAvailable() {
+ return SubscriptionUtil.isSimHardwareVisible(mContext) &&
+ super.isAvailable();
+ }
+
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
+ if (!SubscriptionUtil.isSimHardwareVisible(mContext)) {
+ return;
+ }
final Preference preference = screen.findPreference(getPreferenceKey());
if (!isAvailable() || preference == null || !preference.isVisible()) {
return;
diff --git a/src/com/android/settings/display/ControlsPrivacyPreferenceController.java b/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
index 6f146d5968b97572a071af0b52e07bb9c8da7c15..5b5b900800d3e5fcd4be1a6a6c66540e1a46eb46 100644
--- a/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
@@ -62,6 +62,11 @@ public class ControlsPrivacyPreferenceController extends TogglePreferenceControl
@Override
public int getAvailabilityStatus() {
+ // hide if we should use customizable lock screen quick affordances
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
// hide if lockscreen isn't secure for this user
return isEnabled() && isSecure() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
diff --git a/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java b/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
index 57f717b99137be765728e9daeee5a131140c0f05..be2de59b4b5063ee9e239c7387375813f4d2dd19 100644
--- a/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
@@ -50,9 +50,11 @@ public class ControlsTrivialPrivacyPreferenceController extends TogglePreference
@Override
public CharSequence getSummary() {
- if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
+ if (!CustomizableLockScreenUtils.isFeatureEnabled(mContext)
+ && getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
return mContext.getText(R.string.lockscreen_trivial_disabled_controls_summary);
}
+
return mContext.getText(R.string.lockscreen_trivial_controls_summary);
}
@@ -74,13 +76,18 @@ public class ControlsTrivialPrivacyPreferenceController extends TogglePreference
}
private boolean showDeviceControlsSettingsEnabled() {
- return Settings.Secure.getInt(mContext.getContentResolver(), DEPENDENCY_SETTING_KEY, 0)
- != 0;
+ return CustomizableLockScreenUtils.isFeatureEnabled(mContext)
+ || Settings.Secure.getInt(
+ mContext.getContentResolver(), DEPENDENCY_SETTING_KEY, 0) != 0;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return;
+ }
+
Preference currentPreference = screen.findPreference(getPreferenceKey());
currentPreference.setDependency("lockscreen_privacy_controls_switch");
}
diff --git a/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java b/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c06a51853c9d7a3bdfb2b6813aa4ee05de85031
--- /dev/null
+++ b/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java
@@ -0,0 +1,71 @@
+/*
+ * 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.display;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.PreferenceControllerMixin;
+
+/**
+ * Preference for accessing an experience to customize lock screen quick affordances.
+ */
+public class CustomizableLockScreenQuickAffordancesPreferenceController extends
+ BasePreferenceController implements PreferenceControllerMixin {
+
+ public CustomizableLockScreenQuickAffordancesPreferenceController(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return CustomizableLockScreenUtils.isFeatureEnabled(mContext)
+ ? AVAILABLE
+ : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ final Preference preference = screen.findPreference(getPreferenceKey());
+ if (preference != null) {
+ preference.setOnPreferenceClickListener(preference1 -> {
+ final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
+ final String packageName =
+ mContext.getString(R.string.config_wallpaper_picker_package);
+ if (!TextUtils.isEmpty(packageName)) {
+ intent.setPackage(packageName);
+ }
+ intent.putExtra("destination", "quick_affordances");
+ mContext.startActivity(intent);
+ return true;
+ });
+ refreshSummary(preference);
+ }
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return CustomizableLockScreenUtils.getQuickAffordanceSummary(mContext);
+ }
+}
diff --git a/src/com/android/settings/display/CustomizableLockScreenUtils.java b/src/com/android/settings/display/CustomizableLockScreenUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e9f53d9d0f26c22e632cf91374b64a6afa5da9c
--- /dev/null
+++ b/src/com/android/settings/display/CustomizableLockScreenUtils.java
@@ -0,0 +1,154 @@
+/*
+ * 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.display;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Utilities for display settings related to customizable lock screen features. */
+public final class CustomizableLockScreenUtils {
+
+ private static final String TAG = "CustomizableLockScreenUtils";
+ private static final Uri BASE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.systemui.customization")
+ .build();
+ @VisibleForTesting
+ static final Uri FLAGS_URI = BASE_URI.buildUpon()
+ .path("flags")
+ .build();
+ @VisibleForTesting
+ static final Uri SELECTIONS_URI = BASE_URI.buildUpon()
+ .appendPath("lockscreen_quickaffordance")
+ .appendPath("selections")
+ .build();
+ @VisibleForTesting
+ static final String NAME = "name";
+ @VisibleForTesting
+ static final String VALUE = "value";
+ @VisibleForTesting
+ static final String ENABLED_FLAG =
+ "is_custom_lock_screen_quick_affordances_feature_enabled";
+ @VisibleForTesting
+ static final String AFFORDANCE_NAME = "affordance_name";
+
+ private CustomizableLockScreenUtils() {}
+
+ /**
+ * Queries and returns whether the customizable lock screen quick affordances feature is enabled
+ * on the device.
+ *
+ * This is a slow, blocking call that shouldn't be made on the main thread.
+ */
+ public static boolean isFeatureEnabled(Context context) {
+ try (Cursor cursor = context.getContentResolver().query(
+ FLAGS_URI,
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ Log.w(TAG, "Cursor was null!");
+ return false;
+ }
+
+ final int indexOfNameColumn = cursor.getColumnIndex(NAME);
+ final int indexOfValueColumn = cursor.getColumnIndex(VALUE);
+ if (indexOfNameColumn == -1 || indexOfValueColumn == -1) {
+ Log.w(TAG, "Cursor doesn't contain " + NAME + " or " + VALUE + "!");
+ return false;
+ }
+
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(indexOfNameColumn);
+ final int value = cursor.getInt(indexOfValueColumn);
+ if (TextUtils.equals(ENABLED_FLAG, name)) {
+ Log.d(TAG, ENABLED_FLAG + "=" + value);
+ return value == 1;
+ }
+ }
+
+ Log.w(TAG, "Flag with name \"" + ENABLED_FLAG + "\" not found!");
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while querying quick affordance content provider", e);
+ return false;
+ }
+ }
+
+ /**
+ * Queries and returns a summary text for the currently-selected lock screen quick affordances.
+ *
+ *
This is a slow, blocking call that shouldn't be made on the main thread.
+ */
+ @Nullable
+ public static CharSequence getQuickAffordanceSummary(Context context) {
+ try (Cursor cursor = context.getContentResolver().query(
+ SELECTIONS_URI,
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ Log.w(TAG, "Cursor was null!");
+ return null;
+ }
+
+ final int columnIndex = cursor.getColumnIndex(AFFORDANCE_NAME);
+ if (columnIndex == -1) {
+ Log.w(TAG, "Cursor doesn't contain \"" + AFFORDANCE_NAME + "\" column!");
+ return null;
+ }
+
+ final List affordanceNames = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ final String affordanceName = cursor.getString(columnIndex);
+ if (!TextUtils.isEmpty(affordanceName)) {
+ affordanceNames.add(affordanceName);
+ }
+ }
+
+ // We don't display more than the first two items.
+ final int usableAffordanceNameCount = Math.min(2, affordanceNames.size());
+ final List arguments = new ArrayList<>(usableAffordanceNameCount);
+ if (!affordanceNames.isEmpty()) {
+ arguments.add(affordanceNames.get(0));
+ }
+ if (affordanceNames.size() > 1) {
+ arguments.add(affordanceNames.get(1));
+ }
+
+ return context.getResources().getQuantityString(
+ R.plurals.lockscreen_quick_affordances_summary,
+ usableAffordanceNameCount,
+ arguments.toArray());
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while querying quick affordance content provider", e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/settings/display/QRCodeScannerPreferenceController.java b/src/com/android/settings/display/QRCodeScannerPreferenceController.java
index 16e594a62d5fa9a0bc7b91a3111b4c12db78e2b9..cb022a74fa8888c06ac023dd673e1b161d279efb 100644
--- a/src/com/android/settings/display/QRCodeScannerPreferenceController.java
+++ b/src/com/android/settings/display/QRCodeScannerPreferenceController.java
@@ -87,6 +87,10 @@ public class QRCodeScannerPreferenceController extends TogglePreferenceControlle
@Override
public int getAvailabilityStatus() {
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
return isScannerActivityAvailable() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java
index 914d4be568abb9183060554718f6ca904f37772e..7c4b3aeef03fe07ceac4e4a47088e3d57ccf5715 100644
--- a/src/com/android/settings/display/ScreenResolutionFragment.java
+++ b/src/com/android/settings/display/ScreenResolutionFragment.java
@@ -29,6 +29,8 @@ 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;
@@ -65,6 +67,7 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
private IllustrationPreference mImagePreference;
private DisplayObserver mDisplayObserver;
+ private AccessibilityManager mAccessibilityManager;
@Override
public void onAttach(Context context) {
@@ -72,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);
@@ -215,6 +219,14 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
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);
}
diff --git a/src/com/android/settings/display/ScreenSaverPreferenceController.java b/src/com/android/settings/display/ScreenSaverPreferenceController.java
index 676a567f20232e63ea08227b649f5626d526812c..db4bc37054a9f35e50e8367c0deaa604504959b8 100644
--- a/src/com/android/settings/display/ScreenSaverPreferenceController.java
+++ b/src/com/android/settings/display/ScreenSaverPreferenceController.java
@@ -16,38 +16,42 @@ package com.android.settings.display;
import android.content.Context;
import android.os.UserManager;
-import androidx.preference.Preference;
-
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dream.DreamSettings;
-import com.android.settingslib.core.AbstractPreferenceController;
-public class ScreenSaverPreferenceController extends AbstractPreferenceController implements
+public class ScreenSaverPreferenceController extends BasePreferenceController implements
PreferenceControllerMixin {
- private static final String KEY_SCREEN_SAVER = "screensaver";
+ private final boolean mDreamsDisabledByAmbientModeSuppression;
+
+ public ScreenSaverPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
- public ScreenSaverPreferenceController(Context context) {
- super(context);
+ mDreamsDisabledByAmbientModeSuppression = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
}
@Override
- public boolean isAvailable() {
+ public int getAvailabilityStatus() {
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
- public String getPreferenceKey() {
- return KEY_SCREEN_SAVER;
+ final boolean dreamsOnlyEnabledForDockUser = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
+ // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
+ return (dreamsSupported && (!dreamsOnlyEnabledForDockUser || isSystemUser()))
+ ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
@Override
- public void updateState(Preference preference) {
- preference.setSummary(DreamSettings.getSummaryTextWithDreamName(mContext));
+ public CharSequence getSummary() {
+ if (mDreamsDisabledByAmbientModeSuppression
+ && AmbientDisplayAlwaysOnPreferenceController.isAodSuppressedByBedtime(mContext)) {
+ return mContext.getString(R.string.screensaver_settings_when_to_dream_bedtime);
+ } else {
+ return DreamSettings.getSummaryTextWithDreamName(mContext);
+ }
}
private boolean isSystemUser() {
diff --git a/src/com/android/settings/display/SmartAutoRotateController.java b/src/com/android/settings/display/SmartAutoRotateController.java
index 093c8459cd57ba84579544150ca4028033276211..18aabd80c9e25108468b5669598e2746e2a612da 100644
--- a/src/com/android/settings/display/SmartAutoRotateController.java
+++ b/src/com/android/settings/display/SmartAutoRotateController.java
@@ -180,6 +180,10 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
* Returns true if there is a {@link RotationResolverService} available
*/
public static boolean isRotationResolverServiceAvailable(Context context) {
+ if (!context.getResources().getBoolean(
+ R.bool.config_auto_rotate_face_detection_available)) {
+ return false;
+ }
final PackageManager packageManager = context.getPackageManager();
final String resolvePackage = packageManager.getRotationResolverPackageName();
if (TextUtils.isEmpty(resolvePackage)) {
diff --git a/src/com/android/settings/display/WalletPrivacyPreferenceController.java b/src/com/android/settings/display/WalletPrivacyPreferenceController.java
index 92580f3d569a331dea6ef0ce6ca21fc0fbb8cb81..fe14a40a01aee2dc2aede039de49dc89f89f26ad 100644
--- a/src/com/android/settings/display/WalletPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/WalletPrivacyPreferenceController.java
@@ -62,6 +62,10 @@ public class WalletPrivacyPreferenceController extends TogglePreferenceControlle
@Override
public int getAvailabilityStatus() {
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
return isEnabled() && isSecure() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
diff --git a/src/com/android/settings/dream/DreamAdapter.java b/src/com/android/settings/dream/DreamAdapter.java
index cfee12e0f427ded18f200e23e7ee83a22268de25..b81d6b6d67918e08bc92bf16b06f59f0af5381ac 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 d9c4fb3512ac24377b95f2fb5b945289628ee38b..596fe20f2de412de28097472a6cc77ddaed3c781 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 0000000000000000000000000000000000000000..b72024297694165070b08ecc4bbb80634ddf481d
--- /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 261db6c8ef8dc5e009fa8fc69d15ac7a903f3911..f1a018f8b5113e0cd8fad0b8b26de444ce99b728 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/DreamSettings.java b/src/com/android/settings/dream/DreamSettings.java
index 1d12c1aecd5fbad21a77db418c13532bfbcc69b3..acc69731d0db1a260941747e168a2f568a064cad 100644
--- a/src/com/android/settings/dream/DreamSettings.java
+++ b/src/com/android/settings/dream/DreamSettings.java
@@ -90,12 +90,14 @@ public class DreamSettings extends DashboardFragment implements OnMainSwitchChan
}
}
- static int getDreamSettingDescriptionResId(@WhenToDream int dreamSetting) {
+ static int getDreamSettingDescriptionResId(@WhenToDream int dreamSetting,
+ boolean enabledOnBattery) {
switch (dreamSetting) {
case WHILE_CHARGING:
return R.string.screensaver_settings_summary_sleep;
case WHILE_DOCKED:
- return R.string.screensaver_settings_summary_dock;
+ return enabledOnBattery ? R.string.screensaver_settings_summary_dock
+ : R.string.screensaver_settings_summary_dock_and_charging;
case EITHER:
return R.string.screensaver_settings_summary_either_long;
case NEVER:
@@ -136,10 +138,11 @@ public class DreamSettings extends DashboardFragment implements OnMainSwitchChan
@VisibleForTesting
static CharSequence getSummaryTextFromBackend(DreamBackend backend, Context context) {
- if (!backend.isEnabled()) {
- return context.getString(R.string.screensaver_settings_summary_off);
+ if (backend.isEnabled()) {
+ return context.getString(R.string.screensaver_settings_summary_on,
+ backend.getActiveDreamName());
} else {
- return backend.getActiveDreamName();
+ return context.getString(R.string.screensaver_settings_summary_off);
}
}
diff --git a/src/com/android/settings/dream/IDreamItem.java b/src/com/android/settings/dream/IDreamItem.java
index 49c82bec42afde5b23574eb86e35658580a772c0..911a3cf0bd27c3294c0fc6bcc9d0f48f125c47d9 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/dream/WhenToDreamPicker.java b/src/com/android/settings/dream/WhenToDreamPicker.java
index 1c5e25ebac2a21824fd858de53f3365e03aba119..13cdadf19819c55d7d03c60de212688c24b16be3 100644
--- a/src/com/android/settings/dream/WhenToDreamPicker.java
+++ b/src/com/android/settings/dream/WhenToDreamPicker.java
@@ -32,12 +32,15 @@ public class WhenToDreamPicker extends RadioButtonPickerFragment {
private static final String TAG = "WhenToDreamPicker";
private DreamBackend mBackend;
+ private boolean mDreamsSupportedOnBattery;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mBackend = DreamBackend.getInstance(context);
+ mDreamsSupportedOnBattery = getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsEnabledOnBattery);
}
@Override
@@ -69,11 +72,17 @@ public class WhenToDreamPicker extends RadioButtonPickerFragment {
}
private String[] entries() {
- return getResources().getStringArray(R.array.when_to_start_screensaver_entries);
+ if (mDreamsSupportedOnBattery) {
+ return getResources().getStringArray(R.array.when_to_start_screensaver_entries);
+ }
+ return getResources().getStringArray(R.array.when_to_start_screensaver_entries_no_battery);
}
private String[] keys() {
- return getResources().getStringArray(R.array.when_to_start_screensaver_values);
+ if (mDreamsSupportedOnBattery) {
+ return getResources().getStringArray(R.array.when_to_start_screensaver_values);
+ }
+ return getResources().getStringArray(R.array.when_to_start_screensaver_values_no_battery);
}
@Override
diff --git a/src/com/android/settings/dream/WhenToDreamPreferenceController.java b/src/com/android/settings/dream/WhenToDreamPreferenceController.java
index 4108e850538ecc2958872ce75af8018552ab762a..c3bae0064c98f10140ccb51c0115f3121d03b356 100644
--- a/src/com/android/settings/dream/WhenToDreamPreferenceController.java
+++ b/src/com/android/settings/dream/WhenToDreamPreferenceController.java
@@ -20,7 +20,10 @@ import android.content.Context;
import androidx.preference.Preference;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.display.AmbientDisplayAlwaysOnPreferenceController;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.dream.DreamBackend;
@@ -29,19 +32,39 @@ public class WhenToDreamPreferenceController extends AbstractPreferenceControlle
private static final String WHEN_TO_START = "when_to_start";
private final DreamBackend mBackend;
+ private final boolean mDreamsDisabledByAmbientModeSuppression;
+ private final boolean mDreamsEnabledOnBattery;
WhenToDreamPreferenceController(Context context) {
+ this(context, context.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig),
+ context.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsEnabledOnBattery));
+ }
+
+ @VisibleForTesting
+ WhenToDreamPreferenceController(Context context,
+ boolean dreamsDisabledByAmbientModeSuppression,
+ boolean dreamsEnabledOnBattery) {
super(context);
mBackend = DreamBackend.getInstance(context);
+ mDreamsDisabledByAmbientModeSuppression = dreamsDisabledByAmbientModeSuppression;
+ mDreamsEnabledOnBattery = dreamsEnabledOnBattery;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
- int resId = DreamSettings.getDreamSettingDescriptionResId(mBackend.getWhenToDreamSetting());
- preference.setSummary(preference.getContext().getString(resId));
+ if (mDreamsDisabledByAmbientModeSuppression
+ && AmbientDisplayAlwaysOnPreferenceController.isAodSuppressedByBedtime(mContext)) {
+ preference.setSummary(R.string.screensaver_settings_when_to_dream_bedtime);
+ } else {
+ final int resId = DreamSettings.getDreamSettingDescriptionResId(
+ mBackend.getWhenToDreamSetting(), mDreamsEnabledOnBattery);
+ preference.setSummary(resId);
+ }
}
@Override
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index d096d49dc4ed3295853f502f8d049f041816021e..7d080c9cc931902c3331a420c83e4ccea8d627cf 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -44,15 +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.overlay.FeatureFactory;
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;
@@ -222,8 +223,12 @@ 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,
+ UserHandle userHandle) {
final Bundle args = new Bundle(3);
final PackageManager packageManager = caller.getPackageManager();
args.putString(EXTRA_PACKAGE_NAME, packageName);
@@ -238,7 +243,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
- .setSourceMetricsCategory(fragment.getMetricsCategory())
+ .setSourceMetricsCategory(instrumentable.getMetricsCategory())
+ .setUserHandle(userHandle)
.launch();
}
@@ -295,7 +301,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
notifyBackupManager();
logMetricCategory(selectedPreference);
- mBatteryOptimizeUtils.setAppUsageState(selectedPreference);
Log.d(TAG, "Leave with mode: " + selectedPreference);
}
}
@@ -462,6 +467,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
updatePreferenceState(mUnrestrictedPreference, selectedKey);
updatePreferenceState(mOptimizePreference, selectedKey);
updatePreferenceState(mRestrictedPreference, selectedKey);
+ mBatteryOptimizeUtils.setAppUsageState(getSelectedPreference());
}
private void updatePreferenceState(SelectorWithWidgetPreference preference,
@@ -527,7 +533,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
private CharSequence getAppActiveTime(Bundle bundle) {
final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME);
final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME);
- final int consumedPower = bundle.getInt(EXTRA_POWER_USAGE_AMOUNT);
final int uid = bundle.getInt(EXTRA_UID, 0);
final String slotTime = bundle.getString(EXTRA_SLOT_TIME, null);
final long totalTimeMs = foregroundTimeMs + backgroundTimeMs;
@@ -539,16 +544,11 @@ 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(R.string.battery_usage_without_time);
} 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/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
index 436cde809a8904d33fd401253f66d109903a2106..665be1f0f2c38780ae21710f80ec081e34560f40 100644
--- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
@@ -28,7 +28,6 @@ import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import com.android.settings.Utils;
-import com.android.settings.homepage.contextualcards.slices.BatteryFixSlice;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -99,6 +98,7 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+ intentFilter.addAction(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
final Intent intent = mContext.registerReceiver(this, intentFilter);
updateBatteryStatus(intent, true /* forceUpdate */);
@@ -133,8 +133,9 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver {
mBatteryHealth = batteryHealth;
} else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
+ } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(intent.getAction())) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
}
- BatteryFixSlice.updateBatteryTipAvailabilityCache(mContext);
}
}
\ No newline at end of file
diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java
index b1e4c34e6eaf4d15205fa452d393696bc923c9d0..da3bbe4d37923290f58bca93d751dd8c061752ab 100644
--- a/src/com/android/settings/fuelgauge/BatteryInfo.java
+++ b/src/com/android/settings/fuelgauge/BatteryInfo.java
@@ -49,6 +49,7 @@ public class BatteryInfo {
public CharSequence remainingLabel;
public int batteryLevel;
public int batteryStatus;
+ public int pluggedStatus;
public boolean discharging = true;
public boolean isOverheated;
public long remainingTimeUs = 0;
@@ -253,7 +254,8 @@ public class BatteryInfo {
info.mBatteryUsageStats = batteryUsageStats;
info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
info.batteryPercentString = Utils.formatPercentage(info.batteryLevel);
- info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+ info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
+ info.mCharging = info.pluggedStatus != 0;
info.averageTimeToDischarge = estimate.getAverageDischargeTime();
info.isOverheated = batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN)
@@ -280,25 +282,33 @@ public class BatteryInfo {
BatteryManager.BATTERY_STATUS_UNKNOWN);
info.discharging = false;
info.suggestionLabel = null;
- if (info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL) {
+ int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info);
+ if ((info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL
+ && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
+ || dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) {
+ // Battery defender active, battery charging paused
info.remainingLabel = null;
int chargingLimitedResId = R.string.power_charging_limited;
- info.chargeLabel =
- context.getString(chargingLimitedResId, info.batteryPercentString);
- } else if (chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL) {
+ info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString);
+ } else if ((chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL
+ && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
+ || dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) {
+ // Battery is charging to full
info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs);
- final CharSequence timeString = StringUtil.formatElapsedTime(
- context,
- PowerUtil.convertUsToMs(info.remainingTimeUs),
- false /* withSeconds */,
+ final CharSequence timeString = StringUtil.formatElapsedTime(context,
+ (double) PowerUtil.convertUsToMs(info.remainingTimeUs), false /* withSeconds */,
true /* collapseTimeUnit */);
int resId = R.string.power_charging_duration;
- info.remainingLabel = context.getString(
- R.string.power_remaining_charging_duration_only, timeString);
+ info.remainingLabel = context.getString(R.string.power_remaining_charging_duration_only,
+ timeString);
info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString);
+ } else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) {
+ // Dock defender will be triggered in the future, charging will be optimized.
+ info.chargeLabel = context.getString(R.string.power_charging_future_paused,
+ info.batteryPercentString);
} else {
- final String chargeStatusLabel =
- Utils.getBatteryStatus(context, batteryBroadcast, compactStatus);
+ final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast,
+ compactStatus);
info.remainingLabel = null;
info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString :
resources.getString(R.string.power_charging, info.batteryPercentString,
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index dbfacc6907c2f7c68da5ad774a4e30f8d7f1a23b..e2a4efe9fb8a65fa435c937e89b5f978cba771a5 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -24,7 +24,6 @@ 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;
@@ -222,10 +221,8 @@ public class BatteryOptimizeUtils {
mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
final boolean allowListed = mode == MODE_UNRESTRICTED;
- AsyncTask.execute(() -> {
- setAppOptimizationModeInternal(appOpsManagerMode, allowListed, uid, packageName,
+ setAppOptimizationModeInternal(appOpsManagerMode, allowListed, uid, packageName,
batteryUtils, powerAllowlistBackend);
- });
}
private static void setAppOptimizationModeInternal(
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index a6c48a48d6bae57a0f73b4c44cb7c7f694fb11de..e9b72d0f51a474e7f68274dcdd7f83bd2ace92c3 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -24,6 +24,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.BatteryConsumer;
+import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
@@ -33,6 +34,7 @@ import android.os.Process;
import android.os.SystemClock;
import android.os.UidBatteryConsumer;
import android.os.UserHandle;
+import android.provider.Settings;
import android.util.Log;
import androidx.annotation.IntDef;
@@ -72,6 +74,11 @@ public class BatteryUtils {
/** Special UID for aggregated other users. */
public static final long UID_OTHER_USERS = Long.MIN_VALUE;
+ /** Flag to check if the dock defender mode has been temporarily bypassed */
+ public static final String SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS = "dock_defender_bypass";
+
+ public static final String BYPASS_DOCK_DEFENDER_ACTION = "battery.dock.defender.bypass";
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({StatusType.SCREEN_USAGE,
StatusType.FOREGROUND,
@@ -85,6 +92,18 @@ public class BatteryUtils {
int ALL = 3;
}
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({DockDefenderMode.FUTURE_BYPASS,
+ DockDefenderMode.ACTIVE,
+ DockDefenderMode.TEMPORARILY_BYPASSED,
+ DockDefenderMode.DISABLED})
+ public @interface DockDefenderMode {
+ int FUTURE_BYPASS = 0;
+ int ACTIVE = 1;
+ int TEMPORARILY_BYPASSED = 2;
+ int DISABLED = 3;
+ }
+
private static final String TAG = "BatteryUtils";
private static BatteryUtils sInstance;
@@ -570,4 +589,21 @@ public class BatteryUtils {
return -1L;
}
+
+ /** Gets the current dock defender mode */
+ public static int getCurrentDockDefenderMode(Context context, BatteryInfo batteryInfo) {
+ if (batteryInfo.pluggedStatus == BatteryManager.BATTERY_PLUGGED_DOCK) {
+ if (Settings.Global.getInt(context.getContentResolver(),
+ SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0) == 1) {
+ return DockDefenderMode.TEMPORARILY_BYPASSED;
+ } else if (batteryInfo.isOverheated && FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .isExtraDefend()) {
+ return DockDefenderMode.ACTIVE;
+ } else if (!batteryInfo.isOverheated) {
+ return DockDefenderMode.FUTURE_BYPASS;
+ }
+ }
+ return DockDefenderMode.DISABLED;
+ }
}
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index 83d7a33287abc5e2830d207fd87de8b302f5c55f..9b6f50f17dc5277baea2e16acf3c4132db6f82b1 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -139,10 +139,15 @@ 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.
*/
- Intent getResumeChargeIntent();
+ Intent getResumeChargeIntent(boolean isDockDefender);
/**
* Returns battery history data with corresponding timestamp key.
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 12626414e44168aa929459bfc0920a68b87a7144..cc802e8d30aed4f6419d1074a7ff2fca0a2c2a8a 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -156,10 +156,15 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
}
@Override
- public Intent getResumeChargeIntent() {
+ public Intent getResumeChargeIntent(boolean isDockDefender) {
return null;
}
+ @Override
+ public boolean isExtraDefend() {
+ return false;
+ }
+
@Override
public Map> getBatteryHistory(Context context) {
return null;
diff --git a/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java b/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java
index b960d4c4e06a3d46296b04a7612f4fa3264280c0..b85db40d954cf74fecdcea7395c352958adb8d69 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 d920a8ef777f19e4bd978fd5d86585c6933aef96..d508603c7727bc633832848da7bafabf3c941ae5 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/BatteryTipDialogFragment.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java
index d4c00a44cd2eb02cc402b218a9820d7e48ed037a..5fd3905649fe5a7bd052cd608d6fba397c3399a2 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java
@@ -43,7 +43,6 @@ import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
-import java.text.NumberFormat;
import java.util.List;
/**
@@ -142,29 +141,6 @@ public class BatteryTipDialogFragment extends InstrumentedDialogFragment impleme
.setPositiveButton(R.string.battery_tip_unrestrict_app_dialog_ok, this)
.setNegativeButton(R.string.battery_tip_unrestrict_app_dialog_cancel, null)
.create();
- case BatteryTip.TipType.BATTERY_DEFENDER:
- mMetricsFeatureProvider.action(context,
- SettingsEnums.ACTION_TIP_BATTERY_DEFENDER, mMetricsKey);
- final double chargeLimitLevel = 0.8f;
- final String percentage =
- NumberFormat.getPercentInstance().format(chargeLimitLevel);
- final String message = context.getString(
- R.string.battery_tip_limited_temporarily_dialog_msg, percentage);
- final boolean isPluggedIn = isPluggedIn();
- final AlertDialog.Builder dialogBuilder =
- new AlertDialog.Builder(context)
- .setTitle(R.string.battery_tip_limited_temporarily_title)
- .setMessage(message);
- if (isPluggedIn) {
- dialogBuilder
- .setPositiveButton(
- R.string.battery_tip_limited_temporarily_dialog_resume_charge,
- this)
- .setNegativeButton(R.string.okay, null);
- } else {
- dialogBuilder.setPositiveButton(R.string.okay, null);
- }
- return dialogBuilder.create();
default:
throw new IllegalArgumentException("unknown type " + mBatteryTip.getType());
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
index 4b9858753da98b49bb2c393e01d5f8da370aecca..7bdc5d5686a37610a53e0681a501869dfc144af6 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.detectors.BatteryDefenderDetector;
+import com.android.settings.fuelgauge.batterytip.detectors.DockDefenderDetector;
import com.android.settings.fuelgauge.batterytip.detectors.EarlyWarningDetector;
import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector;
import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector;
@@ -72,7 +73,9 @@ 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());
+ tips.add(new DockDefenderDetector(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 d12784f4ce3d2d553e4c5bb9e6c22b744062cdd4..4dcdc0cc111fb9e7717ee7ff09f48ab1adf2ad5e 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/actions/BatteryDefenderAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java
deleted file mode 100644
index 824b6bee5ca73397ff66dd8ebb4267c5f70b30be..0000000000000000000000000000000000000000
--- a/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2020 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.batterytip.actions;
-
-import android.content.Intent;
-
-import com.android.settings.SettingsActivity;
-import com.android.settings.overlay.FeatureFactory;
-
-/**
- * Action to open the Support Center article
- */
-public class BatteryDefenderAction extends BatteryTipAction {
- private SettingsActivity mSettingsActivity;
-
- public BatteryDefenderAction(SettingsActivity settingsActivity) {
- super(settingsActivity.getApplicationContext());
- mSettingsActivity = settingsActivity;
- }
-
- @Override
- public void handlePositiveAction(int metricsKey) {
- final Intent intent = FeatureFactory.getFactory(mContext)
- .getPowerUsageFeatureProvider(mContext).getResumeChargeIntent();
- if (intent != null) {
- mContext.sendBroadcast(intent);
- }
- }
-}
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
index 5befa330b0a2ca22cca7d2065c376198247ff8c8..08df2e494f63178c951b3bd5f307d6a4e3ba2fb8 100644
--- a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
@@ -16,26 +16,32 @@
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 && !FeatureFactory.getFactory(mContext)
+ .getPowerUsageFeatureProvider(mContext)
+ .isExtraDefend()) {
+ return new BatteryDefenderTip(BatteryTip.StateType.NEW);
+ }
+ return new BatteryDefenderTip(BatteryTip.StateType.INVISIBLE);
}
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a839d392d80bbf75b3711be953d0f8ef6f8f341
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java
@@ -0,0 +1,48 @@
+/*
+ * 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.batterytip.detectors;
+
+import android.content.Context;
+
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+import com.android.settings.fuelgauge.batterytip.tips.DockDefenderTip;
+
+/**
+ * Detect whether the dock defender mode is enabled.
+ */
+public class DockDefenderDetector implements BatteryTipDetector {
+ private final BatteryInfo mBatteryInfo;
+ private final Context mContext;
+
+ public DockDefenderDetector(BatteryInfo batteryInfo, Context context) {
+ mBatteryInfo = batteryInfo;
+ mContext = context;
+ }
+
+ @Override
+ public BatteryTip detect() {
+ int mode = BatteryUtils.getCurrentDockDefenderMode(mContext, mBatteryInfo);
+ return new DockDefenderTip(
+ mode != BatteryUtils.DockDefenderMode.DISABLED
+ ? BatteryTip.StateType.NEW
+ : BatteryTip.StateType.INVISIBLE,
+ mode);
+ }
+
+}
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
index a2890ad9b40c21c68e29ad0e18dfa45ad15b2679..1ccc29c18e3e06411d6cd217ecd30fa8543379b5 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
@@ -18,9 +18,18 @@ 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;
/**
@@ -28,8 +37,10 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
*/
public class BatteryDefenderTip extends BatteryTip {
+ private static final String TAG = "BatteryDefenderTip";
+
public BatteryDefenderTip(@StateType int state) {
- super(TipType.BATTERY_DEFENDER, state, true /* showDialog */);
+ super(TipType.BATTERY_DEFENDER, state, false /* showDialog */);
}
private BatteryDefenderTip(Parcel in) {
@@ -62,6 +73,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(false);
+ 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/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
index 5aee0291e4dace2ed3aaeb079f1eba15eb9c4759..fcf5e09ba1b09f4a2aa864a18bb2a7fb07e72076 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
@@ -58,7 +58,8 @@ public abstract class BatteryTip implements Comparable, Parcelable {
TipType.REDUCED_BATTERY,
TipType.LOW_BATTERY,
TipType.REMOVE_APP_RESTRICTION,
- TipType.BATTERY_DEFENDER})
+ TipType.BATTERY_DEFENDER,
+ TipType.DOCK_DEFENDER})
public @interface TipType {
int SMART_BATTERY_MANAGER = 0;
int APP_RESTRICTION = 1;
@@ -69,6 +70,7 @@ public abstract class BatteryTip implements Comparable, Parcelable {
int SUMMARY = 6;
int REMOVE_APP_RESTRICTION = 7;
int BATTERY_DEFENDER = 8;
+ int DOCK_DEFENDER = 9;
}
@VisibleForTesting
@@ -78,12 +80,13 @@ public abstract class BatteryTip implements Comparable, Parcelable {
TIP_ORDER.append(TipType.BATTERY_SAVER, 0);
TIP_ORDER.append(TipType.LOW_BATTERY, 1);
TIP_ORDER.append(TipType.BATTERY_DEFENDER, 2);
- TIP_ORDER.append(TipType.APP_RESTRICTION, 3);
- TIP_ORDER.append(TipType.HIGH_DEVICE_USAGE, 4);
- TIP_ORDER.append(TipType.SUMMARY, 5);
- TIP_ORDER.append(TipType.SMART_BATTERY_MANAGER, 6);
- TIP_ORDER.append(TipType.REDUCED_BATTERY, 7);
- TIP_ORDER.append(TipType.REMOVE_APP_RESTRICTION, 8);
+ TIP_ORDER.append(TipType.DOCK_DEFENDER, 3);
+ TIP_ORDER.append(TipType.APP_RESTRICTION, 4);
+ TIP_ORDER.append(TipType.HIGH_DEVICE_USAGE, 5);
+ TIP_ORDER.append(TipType.SUMMARY, 6);
+ TIP_ORDER.append(TipType.SMART_BATTERY_MANAGER, 7);
+ TIP_ORDER.append(TipType.REDUCED_BATTERY, 8);
+ TIP_ORDER.append(TipType.REMOVE_APP_RESTRICTION, 9);
}
private static final String KEY_PREFIX = "key_battery_tip";
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc0ba080fc69838083214d49f3f6d5255540beab
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java
@@ -0,0 +1,179 @@
+/*
+ * 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.batterytip.tips;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcel;
+import android.util.Log;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.fuelgauge.BatteryUtils.DockDefenderMode;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.widget.CardPreference;
+import com.android.settingslib.HelpUtils;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+/**
+ * Tip to show dock defender status
+ */
+public class DockDefenderTip extends BatteryTip {
+ private static final String TAG = "DockDefenderTip";
+ private int mMode;
+
+ public DockDefenderTip(@StateType int state, @DockDefenderMode int mode) {
+ super(TipType.DOCK_DEFENDER, state, false);
+ mMode = mode;
+ }
+
+ private DockDefenderTip(Parcel in) {
+ super(in);
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ @Override
+ public CharSequence getTitle(Context context) {
+ switch (mMode) {
+ case DockDefenderMode.FUTURE_BYPASS:
+ return context.getString(R.string.battery_tip_dock_defender_future_bypass_title);
+ case DockDefenderMode.ACTIVE:
+ return context.getString(R.string.battery_tip_dock_defender_active_title);
+ case DockDefenderMode.TEMPORARILY_BYPASSED:
+ return context.getString(
+ R.string.battery_tip_dock_defender_temporarily_bypassed_title);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public CharSequence getSummary(Context context) {
+ switch (mMode) {
+ case DockDefenderMode.FUTURE_BYPASS:
+ return context.getString(R.string.battery_tip_dock_defender_future_bypass_summary);
+ case DockDefenderMode.ACTIVE:
+ return context.getString(R.string.battery_tip_dock_defender_active_summary);
+ case DockDefenderMode.TEMPORARILY_BYPASSED:
+ return context.getString(
+ R.string.battery_tip_dock_defender_temporarily_bypassed_summary);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getIconId() {
+ return mMode == DockDefenderMode.ACTIVE ? R.drawable.ic_battery_status_protected_24dp :
+ R.drawable.ic_battery_dock_defender_untriggered_24dp;
+ }
+
+ @Override
+ public void updateState(BatteryTip tip) {
+ mState = tip.mState;
+ if (tip instanceof DockDefenderTip) {
+ mMode = ((DockDefenderTip) tip).mMode;
+ }
+ }
+
+ @Override
+ public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
+ metricsFeatureProvider.action(context, SettingsEnums.ACTION_DOCK_DEFENDER_TIP,
+ 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);
+ switch (mMode) {
+ case DockDefenderMode.FUTURE_BYPASS:
+ case DockDefenderMode.ACTIVE:
+ cardPreference.setPrimaryButtonText(
+ context.getString(R.string.battery_tip_charge_to_full_button));
+ cardPreference.setPrimaryButtonClickListener(unused -> {
+ resumeCharging(context);
+ mMode = DockDefenderMode.TEMPORARILY_BYPASSED;
+ context.sendBroadcast(new Intent().setAction(
+ BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION).setPackage(
+ context.getPackageName()).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ | Intent.FLAG_RECEIVER_FOREGROUND));
+ updatePreference(preference);
+ });
+ cardPreference.setPrimaryButtonVisible(true);
+ break;
+ case DockDefenderMode.TEMPORARILY_BYPASSED:
+ cardPreference.setPrimaryButtonVisible(false);
+ break;
+ default:
+ cardPreference.setVisible(false);
+ return;
+ }
+
+ cardPreference.setSecondaryButtonText(context.getString(R.string.learn_more));
+ cardPreference.setSecondaryButtonClickListener(
+ button -> button.startActivityForResult(
+ HelpUtils.getHelpIntent(
+ context,
+ context.getString(R.string.help_url_dock_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(true);
+ if (intent != null) {
+ context.sendBroadcast(intent);
+ }
+
+ Log.i(TAG, "send resume charging broadcast intent=" + intent);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public BatteryTip createFromParcel(Parcel in) {
+ return new DockDefenderTip(in);
+ }
+
+ public BatteryTip[] newArray(int size) {
+ return new DockDefenderTip[size];
+ }
+ };
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
index d363308740a8d03eeacd3f7203a4af1eec4aa6dd..56da0f47932da6409fd08d96ba1a653e44a4fce5 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -16,11 +16,12 @@
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.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -28,7 +29,11 @@ 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;
@@ -53,8 +58,6 @@ 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;
@@ -62,27 +65,25 @@ 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 {
+ 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;
- /** 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;
+ 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_CURRENT_TIME_SLOT = "current_time_slot";
+ 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> mBatteryIndexedMap;
+ Map> mBatteryUsageMap;
@VisibleForTesting
Context mPrefContext;
@@ -91,37 +92,52 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@VisibleForTesting
PreferenceGroup mAppListPrefGroup;
@VisibleForTesting
- BatteryChartView mBatteryChartView;
- @VisibleForTesting
ExpandDividerPreference mExpandDividerPreference;
-
@VisibleForTesting
boolean mIsExpanded = false;
+
+ @VisibleForTesting
+ BatteryChartView mDailyChartView;
@VisibleForTesting
- int[] mBatteryHistoryLevels;
+ BatteryChartView mHourlyChartView;
+
@VisibleForTesting
- long[] mBatteryHistoryKeys;
+ int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
- int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
+ int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
- private boolean mIs24HourFormat = false;
+ private boolean mIs24HourFormat;
private boolean mIsFooterPrefAdded = false;
+ private boolean mHourlyChartVisible = true;
+ 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[] mNotAllowShowEntryPackages;
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<>();
- @VisibleForTesting
- final List mSystemEntries = new ArrayList<>();
public BatteryChartPreferenceController(
Context context, String preferenceKey,
@@ -134,10 +150,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mIs24HourFormat = DateFormat.is24HourFormat(context);
mMetricsFeatureProvider =
FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
- mNotAllowShowEntryPackages =
- FeatureFactory.getFactory(context)
- .getPowerUsageFeatureProvider(context)
- .getHideApplicationEntries(context);
mNotAllowShowSummaryPackages =
FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context)
@@ -152,12 +164,14 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
if (savedInstanceState == null) {
return;
}
- mTrapezoidIndex =
- savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+ 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() slotIndex=%d isExpanded=%b",
- mTrapezoidIndex, mIsExpanded));
+ Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+ mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
}
@Override
@@ -179,10 +193,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
if (savedInstance == null) {
return;
}
- savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+ 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() slotIndex=%d isExpanded=%b",
- mTrapezoidIndex, mIsExpanded));
+ Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+ mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
}
@Override
@@ -204,8 +219,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mPrefContext = screen.getContext();
mAppListPrefGroup = screen.findPreference(mPreferenceKey);
mAppListPrefGroup.setOrderingAsAdded(false);
- mAppListPrefGroup.setTitle(
- mPrefContext.getString(R.string.battery_app_usage_for_past_24));
+ mAppListPrefGroup.setTitle("");
mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
// Removes footer first until usage data is loaded to avoid flashing.
if (mFooterPreference != null) {
@@ -249,17 +263,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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;
@@ -272,81 +275,121 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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);
+ 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;
}
- 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;
+ 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;
}
- // Averages the battery level in each time slot to avoid corner conditions.
- float batteryLevelCounter = 0;
- for (BatteryHistEntry entry : entryMap.values()) {
- batteryLevelCounter += entry.mBatteryLevel;
+ 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;
}
- 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();
+ 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();
}
- void setBatteryChartView(final BatteryChartView batteryChartView) {
- if (mBatteryChartView != batteryChartView) {
- mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
+ @VisibleForTesting
+ boolean refreshUi() {
+ if (mDailyChartView == null || mHourlyChartView == null) {
+ // Chart views are not initialized.
+ return false;
}
- }
-
- 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);
- }
+ // 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();
- @VisibleForTesting
- boolean refreshUi(int trapezoidIndex, boolean isForce) {
- // Invalid refresh condition.
- if (mBatteryIndexedMap == null
- || mBatteryChartView == null
- || (mTrapezoidIndex == trapezoidIndex && !isForce)) {
+ if (!refreshUiResult) {
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();
@@ -358,44 +401,79 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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 List entries =
- mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
- addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
- if (entries == null) {
- Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
+ 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;
}
- // 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);
+ if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
+ addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
}
- // Adds the expabable divider if we have system entries data.
- if (!mSystemEntries.isEmpty()) {
+ // Adds the expandable divider if we have system entries data.
+ if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
if (mExpandDividerPreference == null) {
mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
mExpandDividerPreference.setOnExpandListener(this);
@@ -448,6 +526,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
if (!isAdded) {
mAppListPrefGroup.addPreference(pref);
}
+ appIcon.setAlpha(pref.isEnabled() ? ENABLED_ICON_ALPHA : DISABLED_ICON_ALPHA);
prefIndex++;
}
}
@@ -469,11 +548,13 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
private void refreshExpandUi() {
+ final List systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get(
+ mHourlyChartIndex).getSystemDiffEntryList();
if (mIsExpanded) {
- addPreferenceToScreen(mSystemEntries);
+ addPreferenceToScreen(systemEntries);
} else {
// Removes and recycles all system entries to hide all of them.
- for (BatteryDiffEntry entry : mSystemEntries) {
+ for (BatteryDiffEntry entry : systemEntries) {
final String prefKey = entry.mBatteryHistEntry.getKey();
final Preference pref = mAppListPrefGroup.findPreference(prefKey);
if (pref != null) {
@@ -498,12 +579,25 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
}
+ 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_for_past_24)
- : mPrefContext.getString(R.string.battery_system_usage_for_past_24);
+ ? 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)
@@ -511,17 +605,28 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
}
- private String getSlotInformation() {
- if (mTrapezoidIndex < 0) {
+ @VisibleForTesting
+ String getSlotInformation() {
+ if (mDailyViewModel == null || mHourlyViewModels == null) {
+ // No data
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);
+ 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
@@ -575,22 +680,64 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@VisibleForTesting
boolean isValidToShowSummary(String packageName) {
- return !contains(packageName, mNotAllowShowSummaryPackages);
+ return !DataProcessor.contains(packageName, mNotAllowShowSummaryPackages);
}
- @VisibleForTesting
- boolean isValidToShowEntry(String packageName) {
- return !contains(packageName, mNotAllowShowEntryPackages);
+ private void animateBatteryChartViewGroup() {
+ if (mBatteryChartViewGroup != null && mBatteryChartViewGroup.getAlpha() == 0) {
+ mBatteryChartViewGroup.animate().alpha(1f).setDuration(FADE_IN_ANIMATION_DURATION)
+ .start();
+ }
}
- @VisibleForTesting
- void setTimestampLabel() {
- if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
+ private void animateBatteryHourlyChartView(final boolean visible) {
+ if (mHourlyChartView == null || mHourlyChartVisible == visible) {
return;
}
- final long latestTimestamp =
- mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
- mBatteryChartView.setLatestTimestamp(latestTimestamp);
+ mHourlyChartVisible = visible;
+
+ 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) {
@@ -605,60 +752,57 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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;
+ 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 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;
+ static int getTotalHours(final BatteryLevelData batteryLevelData) {
+ if (batteryLevelData == null) {
+ return 0;
}
- return true;
+ 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 getBatteryLast24HrUsageData(Context context) {
+ public static List getAppBatteryUsageData(Context context) {
final long start = System.currentTimeMillis();
final Map> batteryHistoryMap =
FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context)
- .getBatteryHistory(context);
+ .getBatteryHistorySinceLastFullCharge(context);
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
return null;
}
- Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms",
+ Log.d(TAG, String.format("getBatterySinceLastFullChargeUsageData() 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);
+
+ 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 getBatteryLast24HrUsageData(
+ public static BatteryDiffEntry getAppBatteryUsageData(
Context context, String packageName, int userId) {
if (packageName == null) {
return null;
}
- final List entries = getBatteryLast24HrUsageData(context);
+ final List entries = getAppBatteryUsageData(context);
if (entries == null) {
return null;
}
@@ -674,64 +818,35 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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);
+ private final class DailyChartLabelTextGenerator implements
+ BatteryChartViewModel.LabelTextGenerator {
+ @Override
+ public String generateText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeDayOfWeek(mContext,
+ timestamps.get(index), /* isAbbreviation= */ true);
}
- 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
+ public String generateFullText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeDayOfWeek(mContext,
+ timestamps.get(index), /* isAbbreviation= */ false);
}
+ }
+ private final class HourlyChartLabelTextGenerator implements
+ BatteryChartViewModel.LabelTextGenerator {
@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;
+ public String generateText(List timestamps, int index) {
+ return ConvertUtils.utcToLocalTimeHour(mContext, timestamps.get(index),
+ mIs24HourFormat);
}
@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();
- });
+ 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
index 427388befcd8b43447132e863842dc21a22e4fd4..f84ced76d8834ddd379e115da3424f3f09fd9805 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
@@ -18,8 +18,8 @@ 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.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -28,96 +28,70 @@ 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.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.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
-import java.time.Clock;
-import java.util.Arrays;
+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,
- AccessibilityManager.AccessibilityStateChangeListener {
+public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
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 {
/** 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 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;
+ private Paint mTextPaint;
+ private AccessibilityNodeProvider mAccessibilityNodeProvider;
+ private BatteryChartView.OnSelectListener mOnSelectListener;
@VisibleForTesting
- Paint mTrapezoidCurvePaint = null;
- private TrapezoidSlot[] mTrapezoidSlots;
+ TrapezoidSlot[] mTrapezoidSlots;
// Records the location to calculate selected index.
- private float mTouchUpEventX = Float.MIN_VALUE;
- private BatteryChartView.OnSelectListener mOnSelectListener;
+ @VisibleForTesting
+ float mTouchUpEventX = Float.MIN_VALUE;
public BatteryChartView(Context context) {
super(context, null);
@@ -128,57 +102,25 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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();
+ requestLayout();
}
- /** 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) {
+ /** Sets the data model of this view. */
+ public void setViewModel(BatteryChartViewModel viewModel) {
+ if (viewModel == null) {
+ mViewModel = null;
+ invalidate();
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);
- }
- }
+ 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. */
@@ -195,29 +137,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
} 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();
}
@@ -226,6 +145,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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(),
@@ -235,15 +155,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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());
+ 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 = maxHeight + round(mTextPadding * 1.5f);
+ mIndent.bottom = maxTop + round(mTextPadding * 2f);
}
Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
} else {
@@ -254,7 +173,12 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
@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);
}
@@ -284,17 +208,30 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
if (mHoveredIndex != trapezoidIndex) {
mHoveredIndex = trapezoidIndex;
invalidate();
+ sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
}
- break;
+ // 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);
}
- return super.onHoverEvent(event);
}
@Override
public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered);
if (!hovered) {
- mHoveredIndex = SELECTED_INDEX_INVALID; // reset
+ mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate();
}
}
@@ -305,79 +242,58 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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);
+ onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX));
}
@Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- updateClickableState();
- mContext.getSystemService(AccessibilityManager.class)
- .addAccessibilityStateChangeListener(/*listener=*/ this);
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ if (mViewModel == null) {
+ return super.getAccessibilityNodeProvider();
+ }
+ if (mAccessibilityNodeProvider == null) {
+ mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider();
+ }
+ return mAccessibilityNodeProvider;
}
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mContext.getSystemService(AccessibilityManager.class)
- .removeAccessibilityStateChangeListener(/*listener=*/ this);
- mHandler.removeCallbacks(mUpdateClickableStateRun);
+ 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);
}
- @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);
+ private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) {
+ ViewParent parent = getParent();
+ if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return false;
}
- invalidate();
+ AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType);
+ accessibilityEvent.setSource(this, virtualDescendantId);
+ accessibilityEvent.setEnabled(true);
+ accessibilityEvent.setClassName(getAccessibilityClassName());
+ accessibilityEvent.setPackageName(getContext().getPackageName());
+ return parent.requestSendAccessibilityEvent(this, accessibilityEvent);
}
- @Override
- public void setClickable(boolean clickable) {
- super.setClickable(mIsSlotsClickabled && clickable);
+ private void sendAccessibilityEventForHover(int eventType) {
+ if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) {
+ sendAccessibilityEvent(mHoveredIndex, eventType);
+ }
}
- @VisibleForTesting
- void setClickableForce(boolean clickable) {
- super.setClickable(clickable);
+ 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) {
@@ -434,10 +350,10 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private void drawPercentage(Canvas canvas, int index, float offsetY) {
if (mTextPaint != null) {
+ mTextPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(
mPercentages[index],
- getWidth() - mPercentageBounds[index].width()
- - mPercentageBounds[index].left,
+ getWidth(),
offsetY + mPercentageBounds[index].height() * .5f,
mTextPaint);
}
@@ -445,9 +361,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private void drawVerticalDividers(Canvas canvas) {
final int width = getWidth() - mIndent.right;
- final int dividerCount = mTrapezoidCount + 1;
+ final int dividerCount = mTrapezoidSlots.length + 1;
final float dividerSpace = dividerCount * mDividerWidth;
- final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
+ 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;
@@ -463,84 +379,154 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
}
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;
+ // 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;
}
- drawTimestamp(canvas, xOffsets);
+ drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
}
}
- 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);
-
+ /** 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 int getTimestampY(int index) {
- return getHeight() - mTimestampsBounds[index].height()
- + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
- + round(mTextPadding * 1.5f);
+ 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 (mLevels == null) {
+ if (mViewModel == null) {
return;
}
final float trapezoidBottom =
getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
- mTrapezoidVOffset;
- final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
+ 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 < mTrapezoidCount; index++) {
+ for (int index = 0; index < mTrapezoidSlots.length; 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;
- }
+ if (!isValidToDraw(mViewModel, index)) {
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);
+ 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 - mLevels[index] * unitHeight);
- final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
+ 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);
@@ -551,27 +537,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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) {
+ 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
@@ -579,15 +552,42 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
return index;
}
}
- return SELECTED_INDEX_INVALID;
+ 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 boolean isValidToDraw(int trapezoidIndex) {
- return mLevels != null
+ private static boolean isTrapezoidIndexValid(
+ @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
+ return viewModel != null
&& trapezoidIndex >= 0
- && trapezoidIndex < mLevels.length - 1
- && mLevels[trapezoidIndex] != 0
- && mLevels[trapezoidIndex + 1] != 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() {
@@ -597,31 +597,66 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
}
- @VisibleForTesting
- static boolean isAccessibilityEnabled(Context context) {
- final AccessibilityManager accessibilityManager =
- context.getSystemService(AccessibilityManager.class);
- if (!accessibilityManager.isEnabled()) {
- return false;
+ 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;
}
- 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);
+
+ @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);
}
}
- return false;
}
// A container class for each trapezoid left and right location.
- private static final class TrapezoidSlot {
+ @VisibleForTesting
+ static final class TrapezoidSlot {
public float mLeft;
public float 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 0000000000000000000000000000000000000000..f58d2415e199e39a5b1db7fe3514a335dc088959
--- /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 0000000000000000000000000000000000000000..b5d4dde883c6ad9890cc26b183eb1f4140915751
--- /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/batteryusage/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
index 24d6dad8f88531276353cef2d0df75cd3a6db13d..ccb2fb786eb3f24853df16a3bedf845e7275d934 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
@@ -574,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);
@@ -598,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:
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
index 34606a5a58319bb1a917f145791d27866ece84f4..83b26150d3932e704ae4b5c0206dcc7da9b131d4 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
@@ -43,6 +43,6 @@ public class BatteryHistoryLoader
public Map> loadInBackground() {
final PowerUsageFeatureProvider powerUsageFeatureProvider =
FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext);
- return powerUsageFeatureProvider.getBatteryHistory(mContext);
+ return powerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext);
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
index e125d17d6a3e2e9bd49ea87aa5ea551aa1aaf159..674822359be81827ea98155d424150f1098d8c5c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
@@ -49,7 +49,8 @@ public class BatteryHistoryPreference extends Preference {
private TextView mSummaryView;
private CharSequence mSummaryContent;
- private BatteryChartView mBatteryChartView;
+ private BatteryChartView mDailyChartView;
+ private BatteryChartView mHourlyChartView;
private BatteryChartPreferenceController mChartPreferenceController;
public BatteryHistoryPreference(Context context, AttributeSet attrs) {
@@ -92,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);
}
}
@@ -105,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 0000000000000000000000000000000000000000..4ff9eeba9b2d1102c3974a0e882480c0c5f7a40e
--- /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/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index 168fe0f5383ede312d1e29e66fe0e6fc353d0828..130357378f1da6b948a38aa7d7e617e2939b7348 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -55,11 +55,6 @@ public final class ConvertUtils {
// 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;
@@ -87,7 +82,7 @@ public final class ConvertUtils {
}
/** Converts to content values */
- public static ContentValues convert(
+ public static ContentValues convertToContentValues(
BatteryEntry entry,
BatteryUsageStats batteryUsageStats,
int batteryLevel,
@@ -130,6 +125,21 @@ 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);
@@ -140,13 +150,22 @@ public final class ConvertUtils {
/** 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. */
@@ -267,7 +286,7 @@ public final class ConvertUtils {
diffEntry.setTotalConsumePower(totalConsumePower);
}
}
- insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
+ insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap);
resolveMultiUsersData(context, resultMap);
if (purgeLowPercentageAndFakeData) {
purgeLowPercentageAndFakeData(context, resultMap);
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 0000000000000000000000000000000000000000..86533ba07c38c2701d382db09c0e13e4c7cc56c9
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -0,0 +1,1311 @@
+/*
+ * 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 long MIN_TIME_SLOT = 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);
+ // If the timestamp diff is smaller than MIN_TIME_SLOT, returns the empty list directly.
+ if (endTime - startTime < MIN_TIME_SLOT) {
+ return dailyTimestampList;
+ }
+ long nextDay = getTimestampOfNextDay(startTime);
+ // Only if the timestamp diff in the first day is bigger than MIN_TIME_SLOT, start from the
+ // first day. Otherwise, start from the second day.
+ if (nextDay - startTime >= MIN_TIME_SLOT) {
+ dailyTimestampList.add(startTime);
+ }
+ while (nextDay < endTime) {
+ dailyTimestampList.add(nextDay);
+ nextDay += DateUtils.DAY_IN_MILLIS;
+ }
+ final long lastDailyTimestamp = dailyTimestampList.get(dailyTimestampList.size() - 1);
+ // Only if the timestamp diff in the last day is bigger than MIN_TIME_SLOT, add the
+ // last day.
+ if (endTime - lastDailyTimestamp >= MIN_TIME_SLOT) {
+ dailyTimestampList.add(endTime);
+ }
+ // The dailyTimestampList must have the start and end timestamp, otherwise, return an empty
+ // list.
+ if (dailyTimestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
+ return new ArrayList<>();
+ }
+ 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 += MIN_TIME_SLOT;
+ }
+ 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 dailyUsageMap = resultMap.get(dailyKey);
+ dailyUsageMap.values().forEach(diffEntryLists -> {
+ if (diffEntryLists == null) {
+ return;
+ }
+ purgeLowPercentageAndFakeData(
+ diffEntryLists.getAppDiffEntryList(), backgroundUsageTimeHideList,
+ notAllowShowEntryPackages);
+ purgeLowPercentageAndFakeData(
+ diffEntryLists.getSystemDiffEntryList(), backgroundUsageTimeHideList,
+ notAllowShowEntryPackages);
+ });
+ });
+ }
+
+ private static void purgeLowPercentageAndFakeData(
+ final List entries,
+ final Set backgroundUsageTimeHideList,
+ final CharSequence[] notAllowShowEntryPackages) {
+ final Iterator iterator = entries.iterator();
+ while (iterator.hasNext()) {
+ final BatteryDiffEntry entry = iterator.next();
+ final String packageName = entry.getPackageName();
+ if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
+ || FAKE_PACKAGE_NAME.equals(packageName)
+ || contains(packageName, notAllowShowEntryPackages)) {
+ iterator.remove();
+ }
+ if (packageName != null
+ && !backgroundUsageTimeHideList.isEmpty()
+ && contains(packageName, backgroundUsageTimeHideList)) {
+ entry.mBackgroundUsageTimeInMs = 0;
+ }
+ }
+ }
+
+ private static boolean isUsageMapValid(
+ final Map> batteryUsageMap,
+ final List hourlyBatteryLevelsPerDay) {
+ if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null
+ || !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) {
+ Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap");
+ return false;
+ }
+ for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
+ if (batteryUsageMap.get(dailyIndex) == null
+ || !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) {
+ Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, "
+ + "daily size is: " + hourlyBatteryLevelsPerDay.size());
+ return false;
+ }
+ if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
+ continue;
+ }
+ final List timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
+ // Length of hourly usage map should be the length of hourly level data - 1.
+ for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
+ if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) {
+ Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, "
+ + "hourly size is: " + (timestamps.size() - 1));
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private static void loadLabelAndIcon(
+ @Nullable final Map> batteryUsageMap) {
+ if (batteryUsageMap == null) {
+ return;
+ }
+ // Pre-loads each BatteryDiffEntry relative icon and label for all slots.
+ final BatteryDiffData batteryUsageMapForAll =
+ batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
+ if (batteryUsageMapForAll != null) {
+ batteryUsageMapForAll.getAppDiffEntryList().forEach(
+ entry -> entry.loadLabelAndIcon());
+ batteryUsageMapForAll.getSystemDiffEntryList().forEach(
+ entry -> entry.loadLabelAndIcon());
+ }
+ }
+
+ private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(timestamp);
+ calendar.add(Calendar.DAY_OF_YEAR, dayDiff);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ return calendar.getTimeInMillis();
+ }
+
+ private static int getCountOfApps(final Map> resultMap) {
+ final BatteryDiffData diffDataList =
+ resultMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
+ return diffDataList == null
+ ? 0
+ : diffDataList.getAppDiffEntryList().size()
+ + diffDataList.getSystemDiffEntryList().size();
+ }
+
+ private static boolean contains(String target, Set packageNames) {
+ if (target != null && packageNames != null) {
+ for (CharSequence packageName : packageNames) {
+ if (TextUtils.equals(target, packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static long getDiffValue(long v1, long v2, long v3) {
+ return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
+ }
+
+ private static double getDiffValue(double v1, double v2, double v3) {
+ return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
+ }
+
+ @Nullable
+ private static BatteryHistEntry selectBatteryHistEntry(
+ final BatteryHistEntry... batteryHistEntries) {
+ for (BatteryHistEntry entry : batteryHistEntries) {
+ if (entry != null && entry != EMPTY_BATTERY_HIST_ENTRY) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ private static BatteryDiffEntry createOtherUsersEntry(
+ Context context, final double consumePower) {
+ 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, ConvertUtils.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));
+ return batteryDiffEntry;
+ }
+
+ private static void logAppCountMetrics(
+ Context context, final int countOfAppBeforePurge, final int countOfAppAfterPurge) {
+ context = context.getApplicationContext();
+ final MetricsFeatureProvider metricsFeatureProvider =
+ FeatureFactory.getFactory(context).getMetricsFeatureProvider();
+ metricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ countOfAppAfterPurge);
+ metricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ countOfAppBeforePurge - countOfAppAfterPurge);
+ }
+
+ private static void log(Context context, final String content, final long timestamp,
+ final BatteryHistEntry entry) {
+ if (DEBUG) {
+ Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s",
+ utcToLocalTime(context, timestamp), content, entry));
+ }
+ }
+
+ // Compute diff map and loads all items (icon and label) in the background.
+ private static class ComputeUsageMapAndLoadItemsTask
+ extends AsyncTask>> {
+
+ Context mApplicationContext;
+ final Handler mHandler;
+ final UsageMapAsyncResponse mAsyncResponseDelegate;
+ private List mHourlyBatteryLevelsPerDay;
+ private Map> mBatteryHistoryMap;
+
+ private ComputeUsageMapAndLoadItemsTask(
+ Context context,
+ Handler handler,
+ final UsageMapAsyncResponse asyncResponseDelegate,
+ final List hourlyBatteryLevelsPerDay,
+ final Map> batteryHistoryMap) {
+ mApplicationContext = context.getApplicationContext();
+ mHandler = handler;
+ mAsyncResponseDelegate = asyncResponseDelegate;
+ mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
+ mBatteryHistoryMap = batteryHistoryMap;
+ }
+
+ @Override
+ protected Map> doInBackground(Void... voids) {
+ if (mApplicationContext == null
+ || mHandler == null
+ || mAsyncResponseDelegate == null
+ || mBatteryHistoryMap == null
+ || mHourlyBatteryLevelsPerDay == null) {
+ Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()");
+ return null;
+ }
+ final long startTime = System.currentTimeMillis();
+ final Map> batteryUsageMap =
+ getBatteryUsageMap(
+ mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap);
+ loadLabelAndIcon(batteryUsageMap);
+ Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms",
+ (System.currentTimeMillis() - startTime)));
+ return batteryUsageMap;
+ }
+
+ @Override
+ protected void onPostExecute(
+ final Map> batteryUsageMap) {
+ mApplicationContext = null;
+ mHourlyBatteryLevelsPerDay = null;
+ mBatteryHistoryMap = null;
+ // Post results back to main thread to refresh UI.
+ if (mHandler != null && mAsyncResponseDelegate != null) {
+ mHandler.post(() -> {
+ mAsyncResponseDelegate.onBatteryUsageMapLoaded(batteryUsageMap);
+ });
+ }
+ }
+ }
+
+ // Loads battery usage data from battery stats service directly and loads all items (icon and
+ // label) in the background.
+ private static final class LoadUsageMapFromBatteryStatsServiceTask
+ extends ComputeUsageMapAndLoadItemsTask {
+
+ private LoadUsageMapFromBatteryStatsServiceTask(
+ Context context,
+ Handler handler,
+ final UsageMapAsyncResponse asyncResponseDelegate) {
+ super(context, handler, asyncResponseDelegate, /*hourlyBatteryLevelsPerDay=*/ null,
+ /*batteryHistoryMap=*/ null);
+ }
+
+ @Override
+ protected Map> doInBackground(Void... voids) {
+ if (mApplicationContext == null
+ || mHandler == null
+ || mAsyncResponseDelegate == null) {
+ Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()");
+ return null;
+ }
+ final long startTime = System.currentTimeMillis();
+ final Map> batteryUsageMap =
+ getBatteryUsageMapFromStatsService(mApplicationContext);
+ loadLabelAndIcon(batteryUsageMap);
+ Log.d(TAG, String.format("execute LoadUsageMapFromBatteryStatsServiceTask in %d/ms",
+ (System.currentTimeMillis() - startTime)));
+ return batteryUsageMap;
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
index b88d85ddd1fbb3bd789a4551fefb63f64f4ed3d4..db904730a277c6f208fcbb8b35481effc49a532c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
@@ -55,7 +55,6 @@ public class PowerUsageAdvanced extends PowerUsageBase {
private static final String KEY_REFRESH_TYPE = "refresh_type";
private static final String KEY_BATTERY_GRAPH = "battery_graph";
private static final String KEY_APP_LIST = "app_list";
- private static final int LOADER_BATTERY_USAGE_STATS = 2;
@VisibleForTesting
BatteryHistoryPreference mHistPref;
@@ -188,7 +187,7 @@ public class PowerUsageAdvanced extends PowerUsageBase {
// Uses customized battery history loader if chart design is enabled.
if (mIsChartGraphEnabled && !mIsChartDataLoaded) {
mIsChartDataLoaded = true;
- getLoaderManager().restartLoader(LOADER_BATTERY_USAGE_STATS, bundle,
+ restartLoader(LoaderIndex.BATTERY_HISTORY_LOADER, bundle,
mBatteryHistoryLoaderCallbacks);
} else if (!mIsChartGraphEnabled) {
super.restartBatteryStatsLoader(refreshType);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
index ccefdf2c4db59fae0d1655394b83b2cd7a626e63..ed3a921535279cdff8087ba6f0a76447048a3e2f 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageBase.java
@@ -24,6 +24,7 @@ import android.os.Bundle;
import android.os.UserManager;
import android.util.Log;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.loader.app.LoaderManager;
@@ -33,17 +34,19 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.BatteryBroadcastReceiver;
import com.android.settings.fuelgauge.BatteryUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Common base class for things that need to show the battery usage graph.
*/
public abstract class PowerUsageBase extends DashboardFragment {
-
private static final String TAG = "PowerUsageBase";
- private static final String KEY_REFRESH_TYPE = "refresh_type";
- private static final String KEY_INCLUDE_HISTORY = "include_history";
-
- private static final int LOADER_BATTERY_USAGE_STATS = 1;
+ @VisibleForTesting
+ static final String KEY_REFRESH_TYPE = "refresh_type";
+ @VisibleForTesting
+ static final String KEY_INCLUDE_HISTORY = "include_history";
@VisibleForTesting
BatteryUsageStats mBatteryUsageStats;
@@ -55,6 +58,21 @@ public abstract class PowerUsageBase extends DashboardFragment {
final BatteryUsageStatsLoaderCallbacks mBatteryUsageStatsLoaderCallbacks =
new BatteryUsageStatsLoaderCallbacks();
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ LoaderIndex.BATTERY_USAGE_STATS_LOADER,
+ LoaderIndex.BATTERY_INFO_LOADER,
+ LoaderIndex.BATTERY_TIP_LOADER,
+ LoaderIndex.BATTERY_HISTORY_LOADER
+
+ })
+ public @interface LoaderIndex {
+ int BATTERY_USAGE_STATS_LOADER = 0;
+ int BATTERY_INFO_LOADER = 1;
+ int BATTERY_TIP_LOADER = 2;
+ int BATTERY_HISTORY_LOADER = 3;
+ }
+
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
@@ -91,10 +109,28 @@ public abstract class PowerUsageBase extends DashboardFragment {
final Bundle bundle = new Bundle();
bundle.putInt(KEY_REFRESH_TYPE, refreshType);
bundle.putBoolean(KEY_INCLUDE_HISTORY, isBatteryHistoryNeeded());
- getLoaderManager().restartLoader(LOADER_BATTERY_USAGE_STATS, bundle,
+ restartLoader(LoaderIndex.BATTERY_USAGE_STATS_LOADER, bundle,
mBatteryUsageStatsLoaderCallbacks);
}
+ protected LoaderManager getLoaderManagerForCurrentFragment() {
+ return LoaderManager.getInstance(this);
+ }
+
+ protected void restartLoader(int loaderId, Bundle bundle,
+ LoaderManager.LoaderCallbacks> loaderCallbacks) {
+ LoaderManager loaderManager = getLoaderManagerForCurrentFragment();
+ Loader> loader = loaderManager.getLoader(
+ loaderId);
+ if (loader != null && !loader.isReset()) {
+ loaderManager.restartLoader(loaderId, bundle,
+ loaderCallbacks);
+ } else {
+ loaderManager.initLoader(loaderId, bundle,
+ loaderCallbacks);
+ }
+ }
+
protected void onLoadFinished(@BatteryUpdateType int refreshType) {
refreshUi(refreshType);
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
index 405d855d3d8675f62725639347d634007629ad34..f26649278866e4ed79560f931af1f5503ebfd89c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
@@ -64,11 +64,6 @@ public class PowerUsageSummary extends PowerUsageBase implements
@VisibleForTesting
static final String KEY_BATTERY_USAGE = "battery_usage_summary";
- @VisibleForTesting
- static final int BATTERY_INFO_LOADER = 1;
- @VisibleForTesting
- static final int BATTERY_TIP_LOADER = 2;
-
@VisibleForTesting
PowerUsageFeatureProvider mPowerFeatureProvider;
@VisibleForTesting
@@ -241,7 +236,7 @@ public class PowerUsageSummary extends PowerUsageBase implements
@VisibleForTesting
void restartBatteryTipLoader() {
- getLoaderManager().restartLoader(BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks);
+ restartLoader(LoaderIndex.BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks);
}
@VisibleForTesting
@@ -259,10 +254,7 @@ public class PowerUsageSummary extends PowerUsageBase implements
@VisibleForTesting
void initPreference() {
mBatteryUsagePreference = findPreference(KEY_BATTERY_USAGE);
- mBatteryUsagePreference.setSummary(
- mPowerFeatureProvider.isChartGraphEnabled(getContext())
- ? getString(R.string.advanced_battery_preference_summary_with_hours)
- : getString(R.string.advanced_battery_preference_summary));
+ mBatteryUsagePreference.setSummary(getString(R.string.advanced_battery_preference_summary));
mHelpPreference = findPreference(KEY_BATTERY_ERROR);
mHelpPreference.setVisible(false);
@@ -277,8 +269,7 @@ public class PowerUsageSummary extends PowerUsageBase implements
if (!mIsBatteryPresent) {
return;
}
- getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY,
- mBatteryInfoLoaderCallbacks);
+ restartLoader(LoaderIndex.BATTERY_INFO_LOADER, Bundle.EMPTY, mBatteryInfoLoaderCallbacks);
}
@VisibleForTesting
diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java
index 8e14c5a44ea30caa23177c7d6e4721d4b2e3373c..2961abbd89527321ad3186ea4f2102cac1943eef 100644
--- a/src/com/android/settings/homepage/SettingsHomepageActivity.java
+++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java
@@ -27,9 +27,13 @@ import android.app.ActivityManager;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.os.Process;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
@@ -43,6 +47,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toolbar;
+import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
@@ -65,6 +70,8 @@ import com.android.settings.core.CategoryMixin;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.contextualcards.ContextualCardsFragment;
import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.password.PasswordUtils;
+import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
import com.android.settingslib.Utils;
import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
@@ -241,10 +248,21 @@ public class SettingsHomepageActivity extends FragmentActivity implements
if (isFinishing()) {
return;
}
+
+ if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)
+ && (intent.getFlags() & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
+ initSplitPairRules();
+ }
+
// Launch the intent from deep link for large screen devices.
launchDeepLinkIntentToRight();
}
+ @VisibleForTesting
+ void initSplitPairRules() {
+ new ActivityEmbeddingRulesController(getApplicationContext()).initRules();
+ }
+
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -431,6 +449,40 @@ public class SettingsHomepageActivity extends FragmentActivity implements
finish();
return;
}
+
+ ActivityInfo targetActivityInfo = null;
+ try {
+ targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName,
+ /* flags= */ 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Failed to get target ActivityInfo: " + e);
+ finish();
+ return;
+ }
+
+ int callingUid = -1;
+ try {
+ callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken());
+ } catch (RemoteException re) {
+ Log.e(TAG, "Not able to get callingUid: " + re);
+ finish();
+ return;
+ }
+
+ if (!hasPrivilegedAccess(callingUid, targetActivityInfo)) {
+ if (!targetActivityInfo.exported) {
+ Log.e(TAG, "Target Activity is not exported");
+ finish();
+ return;
+ }
+
+ if (!isCallingAppPermitted(targetActivityInfo.permission)) {
+ Log.e(TAG, "Calling app must have the permission of deep link Activity");
+ finish();
+ return;
+ }
+ }
+
targetIntent.setComponent(targetComponentName);
// To prevent launchDeepLinkIntentToRight again for configuration change.
@@ -448,6 +500,19 @@ public class SettingsHomepageActivity extends FragmentActivity implements
targetIntent.setData(intent.getParcelableExtra(
SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA));
+ // Only allow FLAG_GRANT_READ/WRITE_URI_PERMISSION if calling app has the permission to
+ // access specified Uri.
+ int uriPermissionFlags = targetIntent.getFlags()
+ & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (targetIntent.getData() != null
+ && uriPermissionFlags != 0
+ && checkUriPermission(targetIntent.getData(), /* pid= */ -1, callingUid,
+ uriPermissionFlags) == PackageManager.PERMISSION_DENIED) {
+ Log.e(TAG, "Calling app must have the permission to access Uri and grant permission");
+ finish();
+ return;
+ }
+
// Set 2-pane pair rule for the deep link page.
ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
new ComponentName(getApplicationContext(), getClass()),
@@ -472,6 +537,44 @@ public class SettingsHomepageActivity extends FragmentActivity implements
}
}
+ // Check if calling app has privileged access to launch Activity of activityInfo.
+ private boolean hasPrivilegedAccess(int callingUid, ActivityInfo activityInfo) {
+ if (TextUtils.equals(PasswordUtils.getCallingAppPackageName(getActivityToken()),
+ getPackageName())) {
+ return true;
+ }
+
+ int targetUid = -1;
+ try {
+ targetUid = getPackageManager().getApplicationInfo(activityInfo.packageName,
+ /* flags= */ 0).uid;
+ } catch (PackageManager.NameNotFoundException nnfe) {
+ Log.e(TAG, "Not able to get targetUid: " + nnfe);
+ return false;
+ }
+
+ // When activityInfo.exported is false, Activity still can be launched if applications have
+ // the same user ID.
+ if (UserHandle.isSameApp(callingUid, targetUid)) {
+ return true;
+ }
+
+ // When activityInfo.exported is false, Activity still can be launched if calling app has
+ // root or system privilege.
+ int callingAppId = UserHandle.getAppId(callingUid);
+ if (callingAppId == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @VisibleForTesting
+ boolean isCallingAppPermitted(String permission) {
+ return TextUtils.isEmpty(permission) || PasswordUtils.isCallingAppPermitted(
+ this, getActivityToken(), permission);
+ }
+
private String getHighlightMenuKey() {
final Intent intent = getIntent();
if (intent != null && TextUtils.equals(intent.getAction(),
@@ -479,13 +582,30 @@ public class SettingsHomepageActivity extends FragmentActivity implements
final String menuKey = intent.getStringExtra(
EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY);
if (!TextUtils.isEmpty(menuKey)) {
- return menuKey;
+ return maybeRemapMenuKey(menuKey);
}
}
return getString(DEFAULT_HIGHLIGHT_MENU_KEY);
}
- private void reloadHighlightMenuKey() {
+ private String maybeRemapMenuKey(String menuKey) {
+ boolean isPrivacyOrSecurityMenuKey =
+ getString(R.string.menu_key_privacy).equals(menuKey)
+ || getString(R.string.menu_key_security).equals(menuKey);
+ boolean isSafetyCenterMenuKey = getString(R.string.menu_key_safety_center).equals(menuKey);
+
+ if (isPrivacyOrSecurityMenuKey && SafetyCenterManagerWrapper.get().isEnabled(this)) {
+ return getString(R.string.menu_key_safety_center);
+ }
+ if (isSafetyCenterMenuKey && !SafetyCenterManagerWrapper.get().isEnabled(this)) {
+ // We don't know if security or privacy, default to security as it is above.
+ return getString(R.string.menu_key_security);
+ }
+ return menuKey;
+ }
+
+ @VisibleForTesting
+ void reloadHighlightMenuKey() {
mMainFragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
getHighlightMenuKey());
mMainFragment.reloadHighlightMenuKey();
diff --git a/src/com/android/settings/homepage/TopLevelHighlightMixin.java b/src/com/android/settings/homepage/TopLevelHighlightMixin.java
index 4718443f8a4778e0581fe24040e6fef8d15d13b9..db099cbfb98f81a33c258c3e36de3a8786285fa8 100644
--- a/src/com/android/settings/homepage/TopLevelHighlightMixin.java
+++ b/src/com/android/settings/homepage/TopLevelHighlightMixin.java
@@ -114,9 +114,14 @@ public class TopLevelHighlightMixin implements Parcelable, DialogInterface.OnSho
}
Log.d(TAG, "onCreateAdapter, pref key: " + mCurrentKey);
+
+ // Remove the animator to avoid a RecyclerView crash.
+ RecyclerView recyclerView = topLevelSettings.getListView();
+ recyclerView.setItemAnimator(null);
+
mTopLevelAdapter = new HighlightableTopLevelPreferenceAdapter(
(SettingsHomepageActivity) topLevelSettings.getActivity(), preferenceScreen,
- topLevelSettings.getListView(), mCurrentKey, scrollNeeded);
+ recyclerView, mCurrentKey, scrollNeeded);
return mTopLevelAdapter;
}
diff --git a/src/com/android/settings/homepage/TopLevelSettings.java b/src/com/android/settings/homepage/TopLevelSettings.java
index 70530fc8cd33910bcc6ce9dde539407c5f84e592..8c122ef2ad6c1b22446a6162095af78c136dc82d 100644
--- a/src/com/android/settings/homepage/TopLevelSettings.java
+++ b/src/com/android/settings/homepage/TopLevelSettings.java
@@ -43,6 +43,7 @@ import com.android.settings.activityembedding.ActivityEmbeddingRulesController;
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.support.SupportPreferenceController;
import com.android.settings.widget.HomepagePreference;
@@ -157,6 +158,8 @@ public class TopLevelSettings extends DashboardFragment implements SplitLayoutLi
public void onStart() {
if (mFirstStarted) {
mFirstStarted = false;
+ FeatureFactory.getFactory(getContext()).getSearchFeatureProvider().sendPreIndexIntent(
+ getContext());
} else if (mIsEmbeddingActivityEnabled && isOnlyOneActivityInTask()
&& !SplitController.getInstance().isActivityEmbedded(getActivity())) {
// Set default highlight menu key for 1-pane homepage since it will show the placeholder
@@ -346,7 +349,9 @@ public class TopLevelSettings extends DashboardFragment implements SplitLayoutLi
}
private interface PreferenceJob {
- default void init() {}
+ default void init() {
+ }
+
void doForEach(Preference preference);
}
diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
index 067fcf90e00299328d2651772a80ae0d50c16240..81142c205698de231faa69e7243793eabef97fa1 100644
--- a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
+++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
@@ -50,12 +50,6 @@ public class SettingsContextualCardProvider extends ContextualCardProvider {
.setCardName(CustomSliceRegistry.LOW_STORAGE_SLICE_URI.toString())
.setCardCategory(ContextualCard.Category.IMPORTANT)
.build();
- final ContextualCard batteryFixCard =
- ContextualCard.newBuilder()
- .setSliceUri(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString())
- .setCardName(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString())
- .setCardCategory(ContextualCard.Category.IMPORTANT)
- .build();
final String contextualAdaptiveSleepSliceUri =
CustomSliceRegistry.CONTEXTUAL_ADAPTIVE_SLEEP_URI.toString();
final ContextualCard contextualAdaptiveSleepCard =
@@ -80,7 +74,6 @@ public class SettingsContextualCardProvider extends ContextualCardProvider {
.addCard(wifiCard)
.addCard(connectedDeviceCard)
.addCard(lowStorageCard)
- .addCard(batteryFixCard)
.addCard(contextualAdaptiveSleepCard)
.addCard(contextualFaceSettingsCard)
.addCard(darkThemeCard)
diff --git a/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java
index b59fda48bbc69322c3c6e18f943111842fc60e2f..dd788074313d0bd2145bcf4aa0cf160cf0293104 100644
--- a/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java
+++ b/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java
@@ -106,6 +106,9 @@ public class HotspotConditionController implements ConditionalCardController {
@Override
public void startMonitoringStateChange() {
mAppContext.registerReceiver(mReceiver, WIFI_AP_STATE_FILTER);
+ // The intent WIFI_AP_STATE_CHANGED_ACTION is not sticky intent anymore after SC-V2
+ // Handle the initial state after register the receiver.
+ mConditionManager.onConditionChanged();
}
@Override
diff --git a/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java b/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java
deleted file mode 100644
index 33dcd4e79726e0d67c4af0a593db428bef5666b3..0000000000000000000000000000000000000000
--- a/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright (C) 2018 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.homepage.contextualcards.slices;
-
-import static android.content.Context.MODE_PRIVATE;
-
-import static com.android.settings.slices.CustomSliceRegistry.BATTERY_FIX_SLICE_URI;
-
-import android.app.PendingIntent;
-import android.app.settings.SettingsEnums;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.BatteryUsageStats;
-import android.util.ArrayMap;
-import android.view.View;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.slice.Slice;
-import androidx.slice.builders.ListBuilder;
-import androidx.slice.builders.ListBuilder.RowBuilder;
-import androidx.slice.builders.SliceAction;
-
-import com.android.settings.R;
-import com.android.settings.SubSettings;
-import com.android.settings.Utils;
-import com.android.settings.fuelgauge.batterytip.BatteryTipLoader;
-import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
-import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
-import com.android.settings.fuelgauge.batteryusage.BatteryUsageStatsLoader;
-import com.android.settings.fuelgauge.batteryusage.PowerUsageSummary;
-import com.android.settings.slices.CustomSliceable;
-import com.android.settings.slices.SliceBackgroundWorker;
-import com.android.settings.slices.SliceBuilderUtils;
-import com.android.settingslib.utils.ThreadUtils;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-public class BatteryFixSlice implements CustomSliceable {
-
- @VisibleForTesting
- static final String PREFS = "battery_fix_prefs";
- @VisibleForTesting
- static final String KEY_CURRENT_TIPS_TYPE = "current_tip_type";
- static final String KEY_CURRENT_TIPS_STATE = "current_tip_state";
-
- // A map tracking which BatteryTip and which state of that tip is not important.
- private static final Map> UNIMPORTANT_BATTERY_TIPS;
-
- static {
- UNIMPORTANT_BATTERY_TIPS = new ArrayMap<>();
- UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.SUMMARY,
- Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED));
- UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.HIGH_DEVICE_USAGE,
- Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED));
- UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.BATTERY_SAVER,
- Arrays.asList(BatteryTip.StateType.HANDLED));
- }
-
- private static final String TAG = "BatteryFixSlice";
-
- private final Context mContext;
-
- public BatteryFixSlice(Context context) {
- mContext = context;
- }
-
- @Override
- public Uri getUri() {
- return BATTERY_FIX_SLICE_URI;
- }
-
- @Override
- public Slice getSlice() {
- final ListBuilder sliceBuilder =
- new ListBuilder(mContext, BATTERY_FIX_SLICE_URI, ListBuilder.INFINITY)
- .setAccentColor(COLOR_NOT_TINTED);
-
- if (!isBatteryTipAvailableFromCache(mContext)) {
- return buildBatteryGoodSlice(sliceBuilder, true /* isError */);
- }
-
- final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance(getUri());
- final List batteryTips = worker != null ? worker.getResults() : null;
-
- if (batteryTips == null) {
- // Because we need wait slice background worker return data
- return buildBatteryGoodSlice(sliceBuilder, false /* isError */);
- }
-
- for (BatteryTip batteryTip : batteryTips) {
- if (batteryTip.getState() == BatteryTip.StateType.INVISIBLE) {
- continue;
- }
- final Drawable drawable = mContext.getDrawable(batteryTip.getIconId());
- final int iconTintColorId = batteryTip.getIconTintColorId();
- if (iconTintColorId != View.NO_ID) {
- drawable.setColorFilter(new PorterDuffColorFilter(
- mContext.getResources().getColor(iconTintColorId),
- PorterDuff.Mode.SRC_IN));
- }
-
- final IconCompat icon = Utils.createIconWithDrawable(drawable);
- final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(),
- icon,
- ListBuilder.ICON_IMAGE,
- batteryTip.getTitle(mContext));
- sliceBuilder.addRow(new RowBuilder()
- .setTitleItem(icon, ListBuilder.ICON_IMAGE)
- .setTitle(batteryTip.getTitle(mContext))
- .setSubtitle(batteryTip.getSummary(mContext))
- .setPrimaryAction(primaryAction));
- break;
- }
- return sliceBuilder.build();
- }
-
- @Override
- public Intent getIntent() {
- final String screenTitle = mContext.getText(R.string.power_usage_summary_title)
- .toString();
- final Uri contentUri = new Uri.Builder()
- .appendPath(BatteryTipPreferenceController.PREF_NAME).build();
-
- return SliceBuilderUtils.buildSearchResultPageIntent(mContext,
- PowerUsageSummary.class.getName(), BatteryTipPreferenceController.PREF_NAME,
- screenTitle,
- SettingsEnums.SLICE,
- this)
- .setClassName(mContext.getPackageName(), SubSettings.class.getName())
- .setData(contentUri);
- }
-
- @Override
- public int getSliceHighlightMenuRes() {
- return R.string.menu_key_battery;
- }
-
- @Override
- public void onNotifyChange(Intent intent) {
- }
-
- @Override
- public Class getBackgroundWorkerClass() {
- return BatteryTipWorker.class;
- }
-
- private PendingIntent getPrimaryAction() {
- final Intent intent = getIntent();
- return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent,
- PendingIntent.FLAG_IMMUTABLE);
- }
-
- private Slice buildBatteryGoodSlice(ListBuilder sliceBuilder, boolean isError) {
- final IconCompat icon = IconCompat.createWithResource(mContext,
- R.drawable.ic_battery_status_good_24dp);
- final String title = mContext.getString(R.string.power_usage_summary_title);
- final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon,
- ListBuilder.ICON_IMAGE, title);
- sliceBuilder.addRow(new RowBuilder()
- .setTitleItem(icon, ListBuilder.ICON_IMAGE)
- .setTitle(title)
- .setPrimaryAction(primaryAction))
- .setIsError(isError);
- return sliceBuilder.build();
- }
-
- // TODO(b/114807643): we should find a better way to get current battery tip type quickly
- // Now we save battery tip type to shared preference when battery level changes
- public static void updateBatteryTipAvailabilityCache(Context context) {
- ThreadUtils.postOnBackgroundThread(() -> refreshBatteryTips(context));
- }
-
-
- @VisibleForTesting
- static boolean isBatteryTipAvailableFromCache(Context context) {
- final SharedPreferences prefs = context.getSharedPreferences(PREFS, MODE_PRIVATE);
-
- final int type = prefs.getInt(KEY_CURRENT_TIPS_TYPE, BatteryTip.TipType.SUMMARY);
- final int state = prefs.getInt(KEY_CURRENT_TIPS_STATE, BatteryTip.StateType.INVISIBLE);
- if (state == BatteryTip.StateType.INVISIBLE) {
- // State is INVISIBLE, We should not show anything.
- return false;
- }
- final boolean unimportant = UNIMPORTANT_BATTERY_TIPS.containsKey(type)
- && UNIMPORTANT_BATTERY_TIPS.get(type).contains(state);
- return !unimportant;
- }
-
- @WorkerThread
- @VisibleForTesting
- static List refreshBatteryTips(Context context) {
- final BatteryUsageStatsLoader statsLoader = new BatteryUsageStatsLoader(context,
- /* includeBatteryHistory */ false);
- final BatteryUsageStats batteryUsageStats = statsLoader.loadInBackground();
- final BatteryTipLoader loader = new BatteryTipLoader(context, batteryUsageStats);
- final List batteryTips = loader.loadInBackground();
- for (BatteryTip batteryTip : batteryTips) {
- if (batteryTip.getState() != BatteryTip.StateType.INVISIBLE) {
- context.getSharedPreferences(PREFS, MODE_PRIVATE)
- .edit()
- .putInt(KEY_CURRENT_TIPS_TYPE, batteryTip.getType())
- .putInt(KEY_CURRENT_TIPS_STATE, batteryTip.getState())
- .apply();
- break;
- }
- }
- return batteryTips;
- }
-
- public static class BatteryTipWorker extends SliceBackgroundWorker {
-
- private final Context mContext;
-
- public BatteryTipWorker(Context context, Uri uri) {
- super(context, uri);
- mContext = context;
- }
-
- @Override
- protected void onSlicePinned() {
- ThreadUtils.postOnBackgroundThread(() -> {
- final List batteryTips = refreshBatteryTips(mContext);
- updateResults(batteryTips);
- });
- }
-
- @Override
- protected void onSliceUnpinned() {
- }
-
- @Override
- public void close() {
- }
- }
-}
diff --git a/src/com/android/settings/media/MediaOutputIndicatorWorker.java b/src/com/android/settings/media/MediaOutputIndicatorWorker.java
index 09e06723b864a2c328442232d21d99d7cfb34de1..bf1e06e4e0c4c32df3c652b9d19b0354d303a20b 100644
--- a/src/com/android/settings/media/MediaOutputIndicatorWorker.java
+++ b/src/com/android/settings/media/MediaOutputIndicatorWorker.java
@@ -165,6 +165,13 @@ public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements
return mPackageName;
}
+ /** Check if this device supports LE Audio Broadcast feature */
+ public boolean isBroadcastSupported() {
+ LocalBluetoothLeBroadcast broadcast =
+ mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
+ return broadcast != null ? true : false;
+ }
+
public boolean isDeviceBroadcasting() {
LocalBluetoothLeBroadcast broadcast =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
diff --git a/src/com/android/settings/media/MediaOutputUtils.java b/src/com/android/settings/media/MediaOutputUtils.java
index 977c5178165468e87f23e8eac5f419974eaa5ec0..fcb2fb2b524964dbd6dcf4445ba37f9a976bcb38 100644
--- a/src/com/android/settings/media/MediaOutputUtils.java
+++ b/src/com/android/settings/media/MediaOutputUtils.java
@@ -64,7 +64,9 @@ public class MediaOutputUtils {
+ ", play back type : " + pi.getPlaybackType() + ", play back state : "
+ playbackState.getState());
}
- if (playbackState.getState() != PlaybackState.STATE_PLAYING) {
+ if (playbackState.getState() == PlaybackState.STATE_STOPPED
+ || playbackState.getState() == PlaybackState.STATE_NONE
+ || playbackState.getState() == PlaybackState.STATE_ERROR) {
// do nothing
continue;
}
diff --git a/src/com/android/settings/network/CarrierConfigChangedReceiver.java b/src/com/android/settings/network/CarrierConfigChangedReceiver.java
index 8a6d47d96fce07c681bc6f302e511195a04fbfe3..d9ff03efb6ac98fea98bb04d54b577b784e3efb5 100644
--- a/src/com/android/settings/network/CarrierConfigChangedReceiver.java
+++ b/src/com/android/settings/network/CarrierConfigChangedReceiver.java
@@ -32,9 +32,20 @@ public class CarrierConfigChangedReceiver extends BroadcastReceiver {
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
private final CountDownLatch mLatch;
+ private final boolean mIsWaitingForValidSubId;
- public CarrierConfigChangedReceiver(CountDownLatch latch) {
+ /**
+ * This is the CarrierConfigChanged receiver. If it receives the carrier config changed, then it
+ * call the CountDownLatch.countDown().
+ * If the "isWaitingForValidSubId" is true, then the receiver skip the carrier config changed
+ * with the subId = -1. The receiver executes the countDown when the CarrierConfigChanged
+ * with valid subId.
+ * If the "isWaitingForValidSubId" is false, then the receiver executes the countDown when
+ * receiving any CarrierConfigChanged.
+ */
+ public CarrierConfigChangedReceiver(CountDownLatch latch, boolean isWaitingForValidSubId) {
mLatch = latch;
+ mIsWaitingForValidSubId = isWaitingForValidSubId;
}
public void registerOn(Context context) {
@@ -53,7 +64,8 @@ public class CarrierConfigChangedReceiver extends BroadcastReceiver {
}
private void checkSubscriptionIndex(Intent intent) {
- if (intent.hasExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX)) {
+ if (intent.hasExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX)
+ || !mIsWaitingForValidSubId) {
int subId = intent.getIntExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, -1);
Log.i(TAG, "subId from config changed: " + subId);
mLatch.countDown();
diff --git a/src/com/android/settings/network/EraseEuiccDataController.java b/src/com/android/settings/network/EraseEuiccDataController.java
index b1f964b2190a8bbdbb1b9b5a4b62bb6e015009a3..091b6d7748c4f68c0a6361e864eddecf7251d995 100644
--- a/src/com/android/settings/network/EraseEuiccDataController.java
+++ b/src/com/android/settings/network/EraseEuiccDataController.java
@@ -51,7 +51,8 @@ public class EraseEuiccDataController extends BasePreferenceController {
@Override
public int getAvailabilityStatus() {
- return mContext.getPackageManager().hasSystemFeature(
+ return SubscriptionUtil.isSimHardwareVisible(mContext) &&
+ mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY_EUICC) ? AVAILABLE_UNSEARCHABLE
: UNSUPPORTED_ON_DEVICE;
}
diff --git a/src/com/android/settings/network/InternetResetHelper.java b/src/com/android/settings/network/InternetResetHelper.java
index 086ef1b1c5d2747718dcc11956910021dbd54be2..7920cca8f61bb171f4c3abcab63b2efaeaf4d2a4 100644
--- a/src/com/android/settings/network/InternetResetHelper.java
+++ b/src/com/android/settings/network/InternetResetHelper.java
@@ -21,14 +21,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiManager;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
@@ -38,14 +33,14 @@ import androidx.preference.PreferenceCategory;
import com.android.settingslib.connectivity.ConnectivitySubsystemsRecoveryManager;
import com.android.settingslib.utils.HandlerInjector;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to restart connectivity for all requested subsystems.
*/
-public class InternetResetHelper implements LifecycleObserver,
- ConnectivitySubsystemsRecoveryManager.RecoveryStatusCallback {
+public class InternetResetHelper implements LifecycleObserver {
protected static final String TAG = "InternetResetHelper";
public static final long RESTART_TIMEOUT_MS = 15_000; // 15 seconds
@@ -61,41 +56,40 @@ public class InternetResetHelper implements LifecycleObserver,
protected final IntentFilter mWifiStateFilter;
protected final BroadcastReceiver mWifiStateReceiver = new BroadcastReceiver() {
@Override
- @WorkerThread
public void onReceive(Context context, Intent intent) {
- if (intent != null && TextUtils.equals(intent.getAction(),
- WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
- updateWifiStateChange();
- }
+ updateWifiStateChange();
}
};
- protected ConnectivitySubsystemsRecoveryManager mConnectivitySubsystemsRecoveryManager;
- protected HandlerThread mWorkerThread;
- protected boolean mIsRecoveryReady;
- protected boolean mIsWifiReady;
+ protected RecoveryWorker mRecoveryWorker;
+ protected boolean mIsWifiReady = true;
protected HandlerInjector mHandlerInjector;
- protected final Runnable mResumeRunnable = () -> {
- resumePreferences();
- };
protected final Runnable mTimeoutRunnable = () -> {
- mIsRecoveryReady = true;
+ Log.w(TAG, "Resume preferences due to connectivity subsystems recovery timed out.");
+ mRecoveryWorker.clearRecovering();
mIsWifiReady = true;
resumePreferences();
};
- public InternetResetHelper(Context context, Lifecycle lifecycle) {
+ public InternetResetHelper(Context context, Lifecycle lifecycle,
+ NetworkMobileProviderController mobileNetworkController,
+ Preference wifiTogglePreferences,
+ PreferenceCategory connectedWifiEntryPreferenceCategory,
+ PreferenceCategory firstWifiEntryPreferenceCategory,
+ PreferenceCategory wifiEntryPreferenceCategory,
+ Preference resettingPreference) {
mContext = context;
+ mMobileNetworkController = mobileNetworkController;
+ mWifiTogglePreferences = wifiTogglePreferences;
+ mWifiNetworkPreferences.add(connectedWifiEntryPreferenceCategory);
+ mWifiNetworkPreferences.add(firstWifiEntryPreferenceCategory);
+ mWifiNetworkPreferences.add(wifiEntryPreferenceCategory);
+ mResettingPreference = resettingPreference;
+
mHandlerInjector = new HandlerInjector(context.getMainThreadHandler());
mWifiManager = mContext.getSystemService(WifiManager.class);
mWifiStateFilter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION);
-
- mWorkerThread = new HandlerThread(TAG
- + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
- Process.THREAD_PRIORITY_BACKGROUND);
- mWorkerThread.start();
- mConnectivitySubsystemsRecoveryManager = new ConnectivitySubsystemsRecoveryManager(
- mContext, mWorkerThread.getThreadHandler());
+ mRecoveryWorker = RecoveryWorker.getInstance(mContext, this);
if (lifecycle != null) {
lifecycle.addObserver(this);
@@ -118,72 +112,18 @@ public class InternetResetHelper implements LifecycleObserver,
/** @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) */
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
- mHandlerInjector.removeCallbacks(mResumeRunnable);
mHandlerInjector.removeCallbacks(mTimeoutRunnable);
- mWorkerThread.quit();
- }
-
- @Override
- @WorkerThread
- public void onSubsystemRestartOperationBegin() {
- Log.d(TAG, "The connectivity subsystem is starting for recovery.");
- }
-
- @Override
- @WorkerThread
- public void onSubsystemRestartOperationEnd() {
- Log.d(TAG, "The connectivity subsystem is done for recovery.");
- if (!mIsRecoveryReady) {
- mIsRecoveryReady = true;
- mHandlerInjector.postDelayed(mResumeRunnable, 0 /* delayMillis */);
- }
}
@VisibleForTesting
- @WorkerThread
protected void updateWifiStateChange() {
if (!mIsWifiReady && mWifiManager.isWifiEnabled()) {
Log.d(TAG, "The Wi-Fi subsystem is done for recovery.");
mIsWifiReady = true;
- mHandlerInjector.postDelayed(mResumeRunnable, 0 /* delayMillis */);
+ resumePreferences();
}
}
- /**
- * Sets the resetting preference.
- */
- @UiThread
- public void setResettingPreference(Preference preference) {
- mResettingPreference = preference;
- }
-
- /**
- * Sets the mobile network controller.
- */
- @UiThread
- public void setMobileNetworkController(NetworkMobileProviderController controller) {
- mMobileNetworkController = controller;
- }
-
- /**
- * Sets the Wi-Fi toggle preference.
- */
- @UiThread
- public void setWifiTogglePreference(Preference preference) {
- mWifiTogglePreferences = preference;
- }
-
- /**
- * Adds the Wi-Fi network preference.
- */
- @UiThread
- public void addWifiNetworkPreference(PreferenceCategory preference) {
- if (preference != null) {
- mWifiNetworkPreferences.add(preference);
- }
- }
-
- @UiThread
protected void suspendPreferences() {
Log.d(TAG, "Suspend the subsystem preferences");
if (mMobileNetworkController != null) {
@@ -201,9 +141,9 @@ public class InternetResetHelper implements LifecycleObserver,
}
}
- @UiThread
protected void resumePreferences() {
- if (mIsRecoveryReady && mMobileNetworkController != null) {
+ boolean isRecoveryReady = !mRecoveryWorker.isRecovering();
+ if (isRecoveryReady && mMobileNetworkController != null) {
Log.d(TAG, "Resume the Mobile Network controller");
mMobileNetworkController.hidePreference(false /* hide */, true /* immediately */);
}
@@ -214,7 +154,7 @@ public class InternetResetHelper implements LifecycleObserver,
pref.setVisible(true);
}
}
- if (mIsRecoveryReady && mIsWifiReady) {
+ if (isRecoveryReady && mIsWifiReady) {
mHandlerInjector.removeCallbacks(mTimeoutRunnable);
if (mResettingPreference != null) {
Log.d(TAG, "Resume the Resetting preference");
@@ -223,21 +163,99 @@ public class InternetResetHelper implements LifecycleObserver,
}
}
- /**
- * Restart connectivity for all requested subsystems.
- */
- @UiThread
+ protected void showResettingAndSendTimeoutChecks() {
+ suspendPreferences();
+ mHandlerInjector.postDelayed(mTimeoutRunnable, RESTART_TIMEOUT_MS);
+ }
+
+ /** Restart connectivity for all requested subsystems. */
public void restart() {
- if (!mConnectivitySubsystemsRecoveryManager.isRecoveryAvailable()) {
+ if (!mRecoveryWorker.isRecoveryAvailable()) {
Log.e(TAG, "The connectivity subsystem is not available to restart.");
return;
}
-
- Log.d(TAG, "The connectivity subsystem is restarting for recovery.");
- suspendPreferences();
- mIsRecoveryReady = false;
+ showResettingAndSendTimeoutChecks();
mIsWifiReady = !mWifiManager.isWifiEnabled();
- mHandlerInjector.postDelayed(mTimeoutRunnable, RESTART_TIMEOUT_MS);
- mConnectivitySubsystemsRecoveryManager.triggerSubsystemRestart(null /* reason */, this);
+ mRecoveryWorker.triggerRestart();
+ }
+
+ /** Check if the connectivity subsystem is under recovering. */
+ public void checkRecovering() {
+ if (!mRecoveryWorker.isRecovering()) return;
+ mIsWifiReady = false;
+ showResettingAndSendTimeoutChecks();
+ }
+
+ /**
+ * This is a singleton class for ConnectivitySubsystemsRecoveryManager worker.
+ */
+ @VisibleForTesting
+ public static class RecoveryWorker implements
+ ConnectivitySubsystemsRecoveryManager.RecoveryStatusCallback {
+ private static final String TAG = "RecoveryWorker";
+ private static RecoveryWorker sInstance;
+ private static WeakReference sCallback;
+ private static ConnectivitySubsystemsRecoveryManager sRecoveryManager;
+ private static boolean sIsRecovering;
+
+ /**
+ * Create a singleton class for ConnectivitySubsystemsRecoveryManager.
+ *
+ * @param context The context to use for the content resolver.
+ * @param callback The callback of {@link InternetResetHelper} object.
+ * @return an instance of {@link RecoveryWorker} object.
+ */
+ public static RecoveryWorker getInstance(Context context, InternetResetHelper callback) {
+ sCallback = new WeakReference<>(callback);
+ if (sInstance != null) return sInstance;
+
+ sInstance = new RecoveryWorker();
+ Context appContext = context.getApplicationContext();
+ sRecoveryManager = new ConnectivitySubsystemsRecoveryManager(appContext,
+ appContext.getMainThreadHandler());
+ return sInstance;
+ }
+
+ /** Returns true, If the subsystem service is recovering. */
+ public boolean isRecovering() {
+ return sIsRecovering;
+ }
+
+ /** Clear the recovering flag. */
+ public void clearRecovering() {
+ sIsRecovering = false;
+ }
+
+ /** Returns true, If the subsystem service is recovery available. */
+ public boolean isRecoveryAvailable() {
+ return sRecoveryManager.isRecoveryAvailable();
+ }
+
+ /** Trigger connectivity recovery for all requested technologies. */
+ public boolean triggerRestart() {
+ if (!isRecoveryAvailable()) {
+ Log.e(TAG, "The connectivity subsystem is not available to restart.");
+ return false;
+ }
+ sIsRecovering = true;
+ sRecoveryManager.triggerSubsystemRestart(null /* reason */, sInstance);
+ Log.d(TAG, "The connectivity subsystem is restarting for recovery.");
+ return true;
+ }
+
+ @Override
+ public void onSubsystemRestartOperationBegin() {
+ Log.d(TAG, "The connectivity subsystem is starting for recovery.");
+ sIsRecovering = true;
+ }
+
+ @Override
+ public void onSubsystemRestartOperationEnd() {
+ Log.d(TAG, "The connectivity subsystem is done for recovery.");
+ sIsRecovering = false;
+ InternetResetHelper callback = sCallback.get();
+ if (callback == null) return;
+ callback.resumePreferences();
+ }
}
}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.java b/src/com/android/settings/network/MobileNetworkListFragment.java
index 7881690e106d3828c4b60a2fe6435e3a8f020180..ea7031ae1327accb084df7ef11347ce7f363b235 100644
--- a/src/com/android/settings/network/MobileNetworkListFragment.java
+++ b/src/com/android/settings/network/MobileNetworkListFragment.java
@@ -60,6 +60,10 @@ public class MobileNetworkListFragment extends DashboardFragment {
@Override
protected List createPreferenceControllers(Context context) {
final List controllers = new ArrayList<>();
+ if (!SubscriptionUtil.isSimHardwareVisible(getContext())) {
+ finish();
+ return controllers;
+ }
NetworkProviderSimsCategoryController simCategoryPrefCtrl =
new NetworkProviderSimsCategoryController(context, KEY_PREFERENCE_CATEGORY_SIM,
@@ -88,7 +92,8 @@ public class MobileNetworkListFragment extends DashboardFragment {
@Override
protected boolean isPageSearchEnabled(Context context) {
- return context.getSystemService(UserManager.class).isAdminUser();
+ return SubscriptionUtil.isSimHardwareVisible(context) &&
+ context.getSystemService(UserManager.class).isAdminUser();
}
};
}
diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java
index 1a85a7fa3416bb255b6a57a26f5be8c5f2c03bfb..61dd079cb3568812d379929378c1131391199a59 100644
--- a/src/com/android/settings/network/MobileNetworkSummaryController.java
+++ b/src/com/android/settings/network/MobileNetworkSummaryController.java
@@ -206,7 +206,8 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
@Override
public boolean isAvailable() {
- return !Utils.isWifiOnly(mContext) && mUserManager.isAdminUser();
+ return SubscriptionUtil.isSimHardwareVisible(mContext) &&
+ !Utils.isWifiOnly(mContext) && mUserManager.isAdminUser();
}
@Override
diff --git a/src/com/android/settings/network/NetworkProviderCallsSmsController.java b/src/com/android/settings/network/NetworkProviderCallsSmsController.java
index c8b1c49b7bf82926d53c05cdf33d01c9eb8cd548..585c99e0bbe6c63e84e3ab7ea3d0ed2e203f83e3 100644
--- a/src/com/android/settings/network/NetworkProviderCallsSmsController.java
+++ b/src/com/android/settings/network/NetworkProviderCallsSmsController.java
@@ -190,7 +190,8 @@ public class NetworkProviderCallsSmsController extends AbstractPreferenceControl
@Override
public boolean isAvailable() {
- return mUserManager.isAdminUser();
+ return SubscriptionUtil.isSimHardwareVisible(mContext) &&
+ mUserManager.isAdminUser();
}
@Override
diff --git a/src/com/android/settings/network/NetworkProviderCallsSmsFragment.java b/src/com/android/settings/network/NetworkProviderCallsSmsFragment.java
index aad9b10403c9d6f358be290c9e77e42b076568a4..b2cf74e0a828dfb691c07ca92f08b758da7490da 100644
--- a/src/com/android/settings/network/NetworkProviderCallsSmsFragment.java
+++ b/src/com/android/settings/network/NetworkProviderCallsSmsFragment.java
@@ -100,7 +100,8 @@ public class NetworkProviderCallsSmsFragment extends DashboardFragment {
@Override
protected boolean isPageSearchEnabled(Context context) {
- return context.getSystemService(UserManager.class).isAdminUser();
+ return SubscriptionUtil.isSimHardwareVisible(context) &&
+ context.getSystemService(UserManager.class).isAdminUser();
}
};
}
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index ec17dd371b160c590d3e7d35a7c3a275a9ddfd9e..ae2cfbc1a3575c56b573c3609819c372fd2209c7 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -34,6 +34,7 @@ import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
+import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.telephony.TelephonyManager;
@@ -333,6 +334,8 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
addConnectedEthernetNetworkController();
addWifiSwitchPreferenceController();
mWifiStatusMessagePreference = findPreference(PREF_KEY_WIFI_STATUS_MESSAGE);
+
+ checkConnectivityRecovering();
}
private void updateAirplaneModeMsgPreference(boolean visible) {
@@ -341,7 +344,18 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
}
}
+ /**
+ * Whether to show any UI which is SIM related.
+ */
+ @VisibleForTesting
+ boolean showAnySubscriptionInfo(Context context) {
+ return (context != null) && SubscriptionUtil.isSimHardwareVisible(context);
+ }
+
private void addNetworkMobileProviderController() {
+ if (!showAnySubscriptionInfo(getContext())) {
+ return;
+ }
if (mNetworkMobileProviderController == null) {
mNetworkMobileProviderController = new NetworkMobileProviderController(
getContext(), PREF_KEY_PROVIDER_MOBILE_NETWORK);
@@ -367,6 +381,17 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
mWifiSwitchPreferenceController.displayPreference(getPreferenceScreen());
}
+ private void checkConnectivityRecovering() {
+ mInternetResetHelper = new InternetResetHelper(getContext(), getLifecycle(),
+ mNetworkMobileProviderController,
+ findPreference(WifiSwitchPreferenceController.KEY),
+ mConnectedWifiEntryPreferenceCategory,
+ mFirstWifiEntryPreferenceCategory,
+ mWifiEntryPreferenceCategory,
+ mResetInternetPreference);
+ mInternetResetHelper.checkRecovering();
+ }
+
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -598,10 +623,7 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
return;
}
- if (mSelectedWifiEntry.isSaved() && mSelectedWifiEntry.getConnectedState()
- != WifiEntry.CONNECTED_STATE_CONNECTED) {
- menu.add(Menu.NONE, MENU_ID_MODIFY, 0 /* order */, R.string.wifi_modify);
- }
+ addModifyMenuIfSuitable(menu, mSelectedWifiEntry);
}
@VisibleForTesting
@@ -621,6 +643,14 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
}
}
+ @VisibleForTesting
+ void addModifyMenuIfSuitable(ContextMenu menu, WifiEntry wifiEntry) {
+ if (mIsAdmin && wifiEntry.isSaved()
+ && wifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED) {
+ menu.add(Menu.NONE, MENU_ID_MODIFY, 0 /* order */, R.string.wifi_modify);
+ }
+ }
+
private boolean canForgetNetwork() {
return mSelectedWifiEntry.canForget() && !WifiUtils.isNetworkLockedDown(getActivity(),
mSelectedWifiEntry.getWifiConfiguration());
@@ -643,6 +673,12 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
() -> launchWifiDppConfiguratorActivity(mSelectedWifiEntry));
return true;
case MENU_ID_MODIFY:
+ if (!mIsAdmin) {
+ Log.e(TAG, "Can't modify Wi-Fi because the user isn't admin.");
+ EventLog.writeEvent(0x534e4554, "237672190", UserHandle.myUserId(),
+ "User isn't admin");
+ return true;
+ }
showDialog(mSelectedWifiEntry, WifiConfigUiBase2.MODE_MODIFY);
return true;
default:
@@ -1324,6 +1360,12 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
@VisibleForTesting
void launchConfigNewNetworkFragment(WifiEntry wifiEntry) {
+ if (mIsRestricted) {
+ Log.e(TAG, "Can't configure Wi-Fi because NetworkProviderSettings is restricted.");
+ EventLog.writeEvent(0x534e4554, "246301667", -1 /* UID */, "Fragment is restricted.");
+ return;
+ }
+
final Bundle bundle = new Bundle();
bundle.putString(WifiNetworkDetailsFragment.KEY_CHOSEN_WIFIENTRY_KEY,
wifiEntry.getKey());
@@ -1418,16 +1460,6 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
}
private void fixConnectivity() {
- if (mInternetResetHelper == null) {
- mInternetResetHelper = new InternetResetHelper(getContext(), getLifecycle());
- mInternetResetHelper.setResettingPreference(mResetInternetPreference);
- mInternetResetHelper.setMobileNetworkController(mNetworkMobileProviderController);
- mInternetResetHelper.setWifiTogglePreference(
- findPreference(WifiSwitchPreferenceController.KEY));
- mInternetResetHelper.addWifiNetworkPreference(mConnectedWifiEntryPreferenceCategory);
- mInternetResetHelper.addWifiNetworkPreference(mFirstWifiEntryPreferenceCategory);
- mInternetResetHelper.addWifiNetworkPreference(mWifiEntryPreferenceCategory);
- }
mInternetResetHelper.restart();
}
diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java
index 0bba86fafed385d246442f39d1072d8030dfb93d..ccfcfb0a752687a561b4453f508cc3107433eac9 100644
--- a/src/com/android/settings/network/SubscriptionUtil.java
+++ b/src/com/android/settings/network/SubscriptionUtil.java
@@ -86,6 +86,14 @@ public class SubscriptionUtil {
return subscriptions;
}
+ /**
+ * Check if SIM hardware is visible to the end user.
+ */
+ public static boolean isSimHardwareVisible(Context context) {
+ return context.getResources()
+ .getBoolean(R.bool.config_show_sim_info);
+ }
+
@VisibleForTesting
static boolean isInactiveInsertedPSim(UiccSlotInfo slotInfo) {
if (slotInfo == null) {
diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java
index 7ba2e0f8ed0657e232bb86917d135a4f289aac89..7b52680a0dcc96121d4b218e562127eeadf7c3d0 100644
--- a/src/com/android/settings/network/UiccSlotUtil.java
+++ b/src/com/android/settings/network/UiccSlotUtil.java
@@ -28,7 +28,6 @@ import android.telephony.UiccSlotMapping;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.uicc.UiccController;
import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.ImmutableList;
@@ -141,7 +140,8 @@ public class UiccSlotUtil {
inactiveRemovableSlot,
/*removable sim's port Id*/ TelephonyManager.DEFAULT_PORT_INDEX,
excludedLogicalSlotIndex),
- context);
+ context,
+ /*isWaitingForValidSubId=*/ true);
}
/**
@@ -179,7 +179,8 @@ public class UiccSlotUtil {
performSwitchToSlot(telMgr,
prepareUiccSlotMappings(uiccSlotMappings, /*slot is not psim*/ false,
physicalSlotId, port, excludedLogicalSlotIndex),
- context);
+ context,
+ /*isWaitingForValidSubId=*/ false);
}
/**
@@ -231,7 +232,8 @@ public class UiccSlotUtil {
}
private static void performSwitchToSlot(TelephonyManager telMgr,
- Collection uiccSlotMappings, Context context)
+ Collection