From 0432a46627a92e2cdc5b32be225d05411b0faf72 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Mon, 1 Oct 2018 16:47:12 +0800 Subject: [PATCH] Add storage slice in Contextual Settings Homepage - Add storage card that implements CustomSliceable in Contextual Settings Homepage. - Add test case for storage slice. Bug: 117074909, 115971399 Test: visual, robotest, SliceBrowser Change-Id: Idc7d47ba934c2556c124220545ecc73fb2beb7e2 --- ...orageSummaryDonutPreferenceController.java | 22 ++- .../settings/homepage/CardContentLoader.java | 10 ++ .../homepage/deviceinfo/StorageSlice.java | 133 ++++++++++++++++++ .../settings/slices/CustomSliceManager.java | 2 + .../homepage/deviceinfo/StorageSliceTest.java | 83 +++++++++++ 5 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/com/android/settings/homepage/deviceinfo/StorageSlice.java create mode 100644 tests/robotests/src/com/android/settings/homepage/deviceinfo/StorageSliceTest.java diff --git a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java index 1a5152ed400..1dd3d98fe8c 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java @@ -43,6 +43,20 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC super(context); } + /** + * Converts a used storage amount to a formatted text. + * + * @param context Context + * @param usedBytes used bytes of storage + * @return a formatted text. + */ + public static CharSequence convertUsedBytesToFormattedText(Context context, long usedBytes) { + final Formatter.BytesResult result = Formatter.formatBytes(context.getResources(), + usedBytes, 0); + return TextUtils.expandTemplate(context.getText(R.string.storage_size_large_alternate), + result.value, result.units); + } + @Override public void displayPreference(PreferenceScreen screen) { mSummary = (StorageSummaryDonutPreference) screen.findPreference("pref_summary"); @@ -53,11 +67,7 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC public void updateState(Preference preference) { super.updateState(preference); StorageSummaryDonutPreference summary = (StorageSummaryDonutPreference) preference; - final Formatter.BytesResult result = Formatter.formatBytes(mContext.getResources(), - mUsedBytes, 0); - summary.setTitle(TextUtils.expandTemplate( - mContext.getText(R.string.storage_size_large_alternate), result.value, - result.units)); + summary.setTitle(convertUsedBytesToFormattedText(mContext, mUsedBytes)); summary.setSummary(mContext.getString(R.string.storage_volume_total, Formatter.formatShortFileSize(mContext, mTotalBytes))); summary.setPercent(mUsedBytes, mTotalBytes); @@ -83,6 +93,7 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC /** * Updates the state of the donut preference for the next update. + * * @param used Total number of used bytes on the summarized volume. * @param total Total number of bytes on the summarized volume. */ @@ -94,6 +105,7 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC /** * Updates the state of the donut preference for the next update using volume to summarize. + * * @param volume VolumeInfo to use to populate the informayion. */ public void updateSizes(StorageVolumeProvider svp, VolumeInfo volume) { diff --git a/src/com/android/settings/homepage/CardContentLoader.java b/src/com/android/settings/homepage/CardContentLoader.java index 9980503b216..b584084b8e9 100644 --- a/src/com/android/settings/homepage/CardContentLoader.java +++ b/src/com/android/settings/homepage/CardContentLoader.java @@ -26,6 +26,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.homepage.deviceinfo.DataUsageSlice; import com.android.settings.homepage.deviceinfo.DeviceInfoSlice; +import com.android.settings.homepage.deviceinfo.StorageSlice; import com.android.settingslib.utils.AsyncLoaderCompat; import java.util.ArrayList; @@ -112,6 +113,15 @@ public class CardContentLoader extends AsyncLoaderCompat> { .setCardType(ContextualCard.CardType.SLICE) .setIsHalfWidth(true) .build()); + add(new ContextualCard.Builder() + .setSliceUri(StorageSlice.STORAGE_CARD_URI.toString()) + .setName(StorageSlice.PATH_STORAGE_CARD) + .setPackageName(packageName) + .setRankingScore(rankingScore) + .setAppVersion(appVersionCode) + .setCardType(ContextualCard.CardType.SLICE) + .setIsHalfWidth(true) + .build()); }}; return result; } diff --git a/src/com/android/settings/homepage/deviceinfo/StorageSlice.java b/src/com/android/settings/homepage/deviceinfo/StorageSlice.java new file mode 100644 index 00000000000..c9464e40061 --- /dev/null +++ b/src/com/android/settings/homepage/deviceinfo/StorageSlice.java @@ -0,0 +1,133 @@ +/* + * 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.net.Uri; +import android.os.storage.StorageManager; +import android.text.format.Formatter; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.annotations.VisibleForTesting; +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.deviceinfo.StorageDashboardFragment; +import com.android.settings.deviceinfo.storage.StorageSummaryDonutPreferenceController; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; +import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; + +public class StorageSlice implements CustomSliceable { + private static final String TAG = "StorageSlice"; + + /** + * The path denotes the unique name of storage slicel + */ + public static final String PATH_STORAGE_CARD = "storage_card"; + + /** + * Backing Uri for the storage slice. + */ + public static final Uri STORAGE_CARD_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(PATH_STORAGE_CARD) + .build(); + + private final Context mContext; + + public StorageSlice(Context context) { + mContext = context; + } + + @Override + public Uri getUri() { + return STORAGE_CARD_URI; + } + + /** + * Return a storage slice bound to {@link #STORAGE_CARD_URI} + */ + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_homepage_storage); + final String title = mContext.getString(R.string.storage_label); + final SliceAction primaryAction = new SliceAction(getPrimaryAction(), icon, title); + final PrivateStorageInfo info = getPrivateStorageInfo(); + return new ListBuilder(mContext, STORAGE_CARD_URI, ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .setHeader(new ListBuilder.HeaderBuilder().setTitle(title)) + .addRow(new ListBuilder.RowBuilder() + .setTitle(getStorageUsedText(info)) + .setSubtitle(getStorageSummaryText(info)) + .setPrimaryAction(primaryAction)) + .build(); + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.storage_label).toString(); + final Uri contentUri = new Uri.Builder().appendPath(PATH_STORAGE_CARD).build(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + StorageDashboardFragment.class.getName(), PATH_STORAGE_CARD, screenTitle, + MetricsProto.MetricsEvent.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + @VisibleForTesting + PrivateStorageInfo getPrivateStorageInfo() { + final StorageManager storageManager = mContext.getSystemService(StorageManager.class); + final StorageManagerVolumeProvider smvp = new StorageManagerVolumeProvider(storageManager); + return PrivateStorageInfo.getPrivateStorageInfo(smvp); + } + + @VisibleForTesting + CharSequence getStorageUsedText(PrivateStorageInfo info) { + final long usedBytes = info.totalBytes - info.freeBytes; + return StorageSummaryDonutPreferenceController.convertUsedBytesToFormattedText(mContext, + usedBytes); + } + + @VisibleForTesting + CharSequence getStorageSummaryText(PrivateStorageInfo info) { + return mContext.getString(R.string.storage_volume_total, + Formatter.formatShortFileSize(mContext, info.totalBytes)); + } + + @Override + public void onNotifyChange(Intent intent) { + + } +} diff --git a/src/com/android/settings/slices/CustomSliceManager.java b/src/com/android/settings/slices/CustomSliceManager.java index 3d708c722e8..8fa2fb6fdba 100644 --- a/src/com/android/settings/slices/CustomSliceManager.java +++ b/src/com/android/settings/slices/CustomSliceManager.java @@ -22,6 +22,7 @@ import android.util.ArrayMap; 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; @@ -91,5 +92,6 @@ public class CustomSliceManager { mUriMap.put(WifiSlice.WIFI_URI, WifiSlice.class); 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); } } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/homepage/deviceinfo/StorageSliceTest.java b/tests/robotests/src/com/android/settings/homepage/deviceinfo/StorageSliceTest.java new file mode 100644 index 00000000000..5609a7a1e84 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/deviceinfo/StorageSliceTest.java @@ -0,0 +1,83 @@ +/* + * 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.res.Resources; + +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 com.android.settingslib.deviceinfo.PrivateStorageInfo; + +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 StorageSliceTest { + private static final String USED_BYTES_TEXT = "test used bytes"; + private static final String SUMMARY_TEXT = "test summary"; + + private Context mContext; + private StorageSlice mStorageSlice; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mStorageSlice = spy(new StorageSlice(mContext)); + } + + @Test + public void getSlice_shouldBeCorrectSliceContent() { + final PrivateStorageInfo info = new PrivateStorageInfo(100L, 600L); + doReturn(info).when(mStorageSlice).getPrivateStorageInfo(); + doReturn(USED_BYTES_TEXT).when(mStorageSlice).getStorageUsedText(any()); + doReturn(SUMMARY_TEXT).when(mStorageSlice).getStorageSummaryText(any()); + final Slice slice = mStorageSlice.getSlice(); + final SliceMetadata metadata = SliceMetadata.from(mContext, slice); + final SliceAction primaryAction = metadata.getPrimaryAction(); + final IconCompat expectedIcon = IconCompat.createWithResource(mContext, + R.drawable.ic_homepage_storage); + assertThat(primaryAction.getIcon().toString()).isEqualTo(expectedIcon.toString()); + + final List sliceItems = slice.getItems(); + SliceTester.assertTitle(sliceItems, mContext.getString(R.string.storage_label)); + } +}