From 60f6a25af838f8e193a2325b55f98455479ce048 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Fri, 5 Oct 2018 22:11:45 +0800 Subject: [PATCH] Add Battery slice in Contextual Settings Homepage - Add Battery card that implements CustomSliceable in Contextual Settings Homepage. - Add test case for Battery slice. - Created a loadBatteryInfo method for BatterySlice. - Add a map in CustomSliceManager to cache CustomSliceable instances, let existing battery slice be able to update battery info. - Use a flag to avoid triggering an infinite loop when calling notifyChange in the callback function. Bug: 114796623, 115971399 Test: manual, robotests Change-Id: I4b785708bf8456c6c4de7cae4b44f8a060bccbae --- .../settings/fuelgauge/BatteryInfo.java | 91 +++++----- .../settings/homepage/CardContentLoader.java | 21 ++- .../SettingsContextualCardProvider.java | 7 + .../homepage/deviceinfo/BatterySlice.java | 156 ++++++++++++++++++ .../settings/slices/CustomSliceManager.java | 16 +- .../homepage/CardContentLoaderTest.java | 10 +- .../homepage/deviceinfo/BatterySliceTest.java | 77 +++++++++ 7 files changed, 317 insertions(+), 61 deletions(-) create mode 100644 src/com/android/settings/homepage/deviceinfo/BatterySlice.java create mode 100644 tests/robotests/src/com/android/settings/homepage/deviceinfo/BatterySliceTest.java diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index 06cdad6a68e..1f11f5a0cf6 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -148,49 +148,7 @@ public class BatteryInfo { new AsyncTask() { @Override protected BatteryInfo doInBackground(Void... params) { - final BatteryStats stats; - final long batteryStatsTime = System.currentTimeMillis(); - if (statsHelper == null) { - final BatteryStatsHelper localStatsHelper = new BatteryStatsHelper(context, - true); - localStatsHelper.create((Bundle) null); - stats = localStatsHelper.getStats(); - } else { - stats = statsHelper.getStats(); - } - BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); - - final long startTime = System.currentTimeMillis(); - PowerUsageFeatureProvider provider = - FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); - final long elapsedRealtimeUs = - PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); - - Intent batteryBroadcast = context.registerReceiver(null, - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - // 0 means we are discharging, anything else means charging - boolean discharging = - batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; - - if (discharging && provider != null - && provider.isEnhancedBatteryPredictionEnabled(context)) { - Estimate estimate = provider.getEnhancedBatteryPrediction(context); - if (estimate != null) { - BatteryUtils - .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); - return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, - estimate, elapsedRealtimeUs, shortString); - } - } - long prediction = discharging - ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; - Estimate estimate = new Estimate( - PowerUtil.convertUsToMs(prediction), - false, /* isBasedOnUsage */ - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); - BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); - return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, - estimate, elapsedRealtimeUs, shortString); + return getBatteryInfo(context, statsHelper, shortString); } @Override @@ -202,6 +160,53 @@ public class BatteryInfo { }.execute(); } + public static BatteryInfo getBatteryInfo(final Context context, + final BatteryStatsHelper statsHelper, boolean shortString) { + final BatteryStats stats; + final long batteryStatsTime = System.currentTimeMillis(); + if (statsHelper == null) { + final BatteryStatsHelper localStatsHelper = new BatteryStatsHelper(context, + true); + localStatsHelper.create((Bundle) null); + stats = localStatsHelper.getStats(); + } else { + stats = statsHelper.getStats(); + } + BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); + + final long startTime = System.currentTimeMillis(); + PowerUsageFeatureProvider provider = + FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); + final long elapsedRealtimeUs = + PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); + + final Intent batteryBroadcast = context.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + // 0 means we are discharging, anything else means charging + final boolean discharging = + batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; + + if (discharging && provider != null + && provider.isEnhancedBatteryPredictionEnabled(context)) { + Estimate estimate = provider.getEnhancedBatteryPrediction(context); + if (estimate != null) { + BatteryUtils + .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); + return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, + estimate, elapsedRealtimeUs, shortString); + } + } + final long prediction = discharging + ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; + final Estimate estimate = new Estimate( + PowerUtil.convertUsToMs(prediction), + false, /* isBasedOnUsage */ + Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); + return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, + estimate, elapsedRealtimeUs, shortString); + } + @WorkerThread public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryStats stats, long elapsedRealtimeUs, boolean shortString) { diff --git a/src/com/android/settings/homepage/CardContentLoader.java b/src/com/android/settings/homepage/CardContentLoader.java index 401e53cbbbb..3ac7f722aeb 100644 --- a/src/com/android/settings/homepage/CardContentLoader.java +++ b/src/com/android/settings/homepage/CardContentLoader.java @@ -32,6 +32,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.slice.Slice; +import com.android.settings.homepage.deviceinfo.BatterySlice; import com.android.settings.homepage.deviceinfo.DataUsageSlice; import com.android.settings.homepage.deviceinfo.DeviceInfoSlice; import com.android.settingslib.utils.AsyncLoaderCompat; @@ -101,17 +102,15 @@ public class CardContentLoader extends AsyncLoaderCompat> { .setCardType(ContextualCard.CardType.SLICE) .setIsHalfWidth(false) .build()); - //TODO(b/115971399): Will change following values of SliceUri and Name - // after landing these slice cards. -// add(new ContextualCard.Builder() -// .setSliceUri("content://com.android.settings.slices/battery_card") -// .setName(packageName + "/" + "battery_card") -// .setPackageName(packageName) -// .setRankingScore(rankingScore) -// .setAppVersion(appVersionCode) -// .setCardType(ContextualCard.CardType.SLICE) -// .setIsHalfWidth(true) -// .build()); + add(new ContextualCard.Builder() + .setSliceUri(BatterySlice.BATTERY_CARD_URI) + .setName(BatterySlice.PATH_BATTERY_INFO) + .setPackageName(packageName) + .setRankingScore(rankingScore) + .setAppVersion(appVersionCode) + .setCardType(ContextualCard.CardType.SLICE) + .setIsHalfWidth(false) + .build()); add(new ContextualCard.Builder() .setSliceUri(DeviceInfoSlice.DEVICE_INFO_CARD_URI) .setName(DeviceInfoSlice.PATH_DEVICE_INFO) diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java index 26f86b9da5e..0d72cebd3b3 100644 --- a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java @@ -20,6 +20,7 @@ import static android.provider.SettingsSlicesContract.KEY_WIFI; import android.annotation.Nullable; +import com.android.settings.homepage.deviceinfo.BatterySlice; import com.android.settings.homepage.deviceinfo.DataUsageSlice; import com.android.settings.homepage.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.deviceinfo.StorageSlice; @@ -63,12 +64,18 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setSliceUri(EmergencyInfoSlice.EMERGENCY_INFO_CARD_URI.toString()) .setCardName(EmergencyInfoSlice.PATH_EMERGENCY_INFO_CARD) .build(); + final ContextualCard batteryInfoCard = + ContextualCard.newBuilder() + .setSliceUri(BatterySlice.BATTERY_CARD_URI.toSafeString()) + .setCardName(BatterySlice.PATH_BATTERY_INFO) + .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(dataUsageCard) .addCard(deviceInfoCard) .addCard(storageInfoCard) .addCard(emergencyInfoCard) + .addCard(batteryInfoCard) .build(); return cards; diff --git a/src/com/android/settings/homepage/deviceinfo/BatterySlice.java b/src/com/android/settings/homepage/deviceinfo/BatterySlice.java new file mode 100644 index 00000000000..1090b40fe8b --- /dev/null +++ b/src/com/android/settings/homepage/deviceinfo/BatterySlice.java @@ -0,0 +1,156 @@ +/* + * 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.deviceinfo; + +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.PowerManager; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.slices.SliceBuilderUtils; + +/** + * Utility class to build a Battery Slice, and handle all associated actions. + */ +public class BatterySlice implements CustomSliceable { + private static final String TAG = "BatterySlice"; + + /** + * The path denotes the unique name of battery slice. + */ + public static final String PATH_BATTERY_INFO = "battery_card"; + + /** + * Backing Uri for the Battery Slice. + */ + public static final Uri BATTERY_CARD_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(PATH_BATTERY_INFO) + .build(); + + private final Context mContext; + + private BatteryInfo mBatteryInfo; + private boolean mIsBatteryInfoLoading; + + public BatterySlice(Context context) { + mContext = context; + } + + /** + * Return a {@link BatterySlice} bound to {@link #BATTERY_CARD_URI} + */ + @Override + public Slice getSlice() { + if (mBatteryInfo == null) { + mIsBatteryInfoLoading = true; + loadBatteryInfo(); + } + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_settings_battery); + final CharSequence title = mContext.getText(R.string.power_usage_summary_title); + final SliceAction primarySliceAction = new SliceAction(getPrimaryAction(), icon, title); + final Slice slice = new ListBuilder(mContext, BATTERY_CARD_URI, ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .setHeader(new ListBuilder.HeaderBuilder().setTitle(title)) + .addRow(new ListBuilder.RowBuilder() + .setTitle(getBatteryPercentString(), mIsBatteryInfoLoading) + .setSubtitle(getSummary(), mIsBatteryInfoLoading) + .setPrimaryAction(primarySliceAction)) + .build(); + mBatteryInfo = null; + mIsBatteryInfoLoading = false; + return slice; + } + + @Override + public Uri getUri() { + return BATTERY_CARD_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.power_usage_summary_title).toString(); + final Uri contentUri = new Uri.Builder().appendPath(PATH_BATTERY_INFO).build(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + PowerUsageSummary.class.getName(), PATH_BATTERY_INFO, screenTitle, + MetricsProto.MetricsEvent.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + } + + @Override + public IntentFilter getIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + intentFilter.addAction(Intent.ACTION_POWER_CONNECTED); + intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED); + intentFilter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED); + return intentFilter; + } + + @VisibleForTesting + void loadBatteryInfo() { + BatteryInfo.getBatteryInfo(mContext, info -> { + mBatteryInfo = info; + mContext.getContentResolver().notifyChange(getUri(), null); + }, true); + } + + @VisibleForTesting + CharSequence getBatteryPercentString() { + return mBatteryInfo == null ? null : mBatteryInfo.batteryPercentString; + } + + @VisibleForTesting + CharSequence getSummary() { + if (mBatteryInfo == null) { + return null; + } + return mBatteryInfo.remainingLabel == null ? mBatteryInfo.statusLabel + : mBatteryInfo.remainingLabel; + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, + intent, 0 /* flags */); + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/CustomSliceManager.java b/src/com/android/settings/slices/CustomSliceManager.java index 8fa2fb6fdba..a207fccfcb6 100644 --- a/src/com/android/settings/slices/CustomSliceManager.java +++ b/src/com/android/settings/slices/CustomSliceManager.java @@ -20,12 +20,14 @@ import android.content.Context; import android.net.Uri; import android.util.ArrayMap; +import com.android.settings.homepage.deviceinfo.BatterySlice; import com.android.settings.homepage.deviceinfo.DataUsageSlice; import com.android.settings.homepage.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.deviceinfo.StorageSlice; import com.android.settings.wifi.WifiSlice; import java.util.Map; +import java.util.WeakHashMap; /** * Manages custom {@link androidx.slice.Slice Slices}, which are all Slices not backed by @@ -39,10 +41,12 @@ public class CustomSliceManager { protected final Map> mUriMap; private final Context mContext; + private final Map mSliceableCache; public CustomSliceManager(Context context) { mContext = context.getApplicationContext(); mUriMap = new ArrayMap<>(); + mSliceableCache = new WeakHashMap<>(); addSlices(); } @@ -53,13 +57,18 @@ public class CustomSliceManager { * the only thing that should be needed to create the object. */ public CustomSliceable getSliceableFromUri(Uri uri) { - final Class clazz = mUriMap.get(uri); + if (mSliceableCache.containsKey(uri)) { + return mSliceableCache.get(uri); + } + final Class clazz = mUriMap.get(uri); if (clazz == null) { throw new IllegalArgumentException("No Slice found for uri: " + uri); } - return CustomSliceable.createInstance(mContext, clazz); + final CustomSliceable sliceable = CustomSliceable.createInstance(mContext, clazz); + mSliceableCache.put(uri, sliceable); + return sliceable; } /** @@ -93,5 +102,6 @@ public class CustomSliceManager { mUriMap.put(DataUsageSlice.DATA_USAGE_CARD_URI, DataUsageSlice.class); mUriMap.put(DeviceInfoSlice.DEVICE_INFO_CARD_URI, DeviceInfoSlice.class); mUriMap.put(StorageSlice.STORAGE_CARD_URI, StorageSlice.class); + mUriMap.put(BatterySlice.BATTERY_CARD_URI, BatterySlice.class); } -} \ No newline at end of file +} diff --git a/tests/robotests/src/com/android/settings/homepage/CardContentLoaderTest.java b/tests/robotests/src/com/android/settings/homepage/CardContentLoaderTest.java index a7527f37b1f..853cf20ec8a 100644 --- a/tests/robotests/src/com/android/settings/homepage/CardContentLoaderTest.java +++ b/tests/robotests/src/com/android/settings/homepage/CardContentLoaderTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.net.Uri; +import com.android.settings.homepage.deviceinfo.BatterySlice; import com.android.settings.homepage.deviceinfo.DataUsageSlice; import com.android.settings.homepage.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.deviceinfo.StorageSlice; @@ -54,17 +55,18 @@ public class CardContentLoaderTest { } @Test - public void createStaticCards_shouldReturnTwoCards() { + public void createStaticCards_shouldReturnFourCards() { final List defaultData = mCardContentLoader.createStaticCards(); - assertThat(defaultData).hasSize(2); + assertThat(defaultData).hasSize(3); } @Test - public void createStaticCards_shouldContainDataUsageAndDeviceInfo() { + public void createStaticCards_shouldContainCorrectCards() { final Uri dataUsage = DataUsageSlice.DATA_USAGE_CARD_URI; final Uri deviceInfo = DeviceInfoSlice.DEVICE_INFO_CARD_URI; - final List expectedUris = Arrays.asList(dataUsage, deviceInfo); + final Uri batteryInfo = BatterySlice.BATTERY_CARD_URI; + final List expectedUris = Arrays.asList(dataUsage, deviceInfo, batteryInfo); final List actualCardUris = mCardContentLoader.createStaticCards().stream().map( ContextualCard::getSliceUri).collect(Collectors.toList()); diff --git a/tests/robotests/src/com/android/settings/homepage/deviceinfo/BatterySliceTest.java b/tests/robotests/src/com/android/settings/homepage/deviceinfo/BatterySliceTest.java new file mode 100644 index 00000000000..8baaab4af49 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/deviceinfo/BatterySliceTest.java @@ -0,0 +1,77 @@ +/* + * 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.deviceinfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.SliceLiveData; + +import com.android.settings.R; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.SliceTester; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +public class BatterySliceTest { + + private Context mContext; + private BatterySlice mBatterySlice; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mBatterySlice = spy(new BatterySlice(mContext)); + } + + @Test + public void getSlice_shouldBeCorrectSliceContent() { + doNothing().when(mBatterySlice).loadBatteryInfo(); + doReturn("10%").when(mBatterySlice).getBatteryPercentString(); + doReturn("test").when(mBatterySlice).getSummary(); + final Slice slice = mBatterySlice.getSlice(); + final SliceMetadata metadata = SliceMetadata.from(mContext, slice); + final SliceAction primaryAction = metadata.getPrimaryAction(); + final IconCompat expectedIcon = IconCompat.createWithResource(mContext, + R.drawable.ic_settings_battery); + assertThat(primaryAction.getIcon().toString()).isEqualTo(expectedIcon.toString()); + + final List sliceItems = slice.getItems(); + SliceTester.assertTitle(sliceItems, mContext.getString(R.string.power_usage_summary_title)); + } +}