diff --git a/res/values/strings.xml b/res/values/strings.xml index f15eeb2292e..516f1a24770 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10302,4 +10302,7 @@ Keep Remove this suggestion? + + + Storage is low. %1$s used - %2$s free \ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java index c736c4d1487..f58ec7434b5 100644 --- a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import com.android.settings.homepage.contextualcards.deviceinfo.BatterySlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; +import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; import com.android.settings.intelligence.ContextualCardProto.ContextualCard; import com.android.settings.intelligence.ContextualCardProto.ContextualCardList; import com.android.settings.wifi.WifiSlice; @@ -54,10 +55,17 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setCardName(ConnectedDeviceSlice.PATH_CONNECTED_DEVICE) .setCardCategory(ContextualCard.Category.IMPORTANT) .build(); + final ContextualCard lowStorageCard = + ContextualCard.newBuilder() + .setSliceUri(LowStorageSlice.LOW_STORAGE_URI.toString()) + .setCardName(LowStorageSlice.PATH_LOW_STORAGE) + .setCardCategory(ContextualCard.Category.IMPORTANT) + .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(batteryInfoCard) .addCard(connectedDeviceCard) + .addCard(lowStorageCard) .build(); return cards; diff --git a/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java b/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java new file mode 100644 index 00000000000..7f6efccd49e --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java @@ -0,0 +1,144 @@ +/* + * 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 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 android.util.Log; + +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.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.deviceinfo.StorageSettings; +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; + +import java.text.NumberFormat; + +public class LowStorageSlice implements CustomSliceable { + + /** + * The path denotes the unique name of Low storage Slice. + */ + public static final String PATH_LOW_STORAGE = "low_storage"; + + /** + * Backing Uri for Low storage Slice. + */ + public static final Uri LOW_STORAGE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(PATH_LOW_STORAGE) + .build(); + + private static final String TAG = "LowStorageSlice"; + + /** + * If user used >= 85% storage. + */ + private static final double LOW_STORAGE_THRESHOLD = 0.85; + + private final Context mContext; + + public LowStorageSlice(Context context) { + mContext = context; + } + + /** + * Return a Low storage Slice bound to {@link #LOW_STORAGE_URI} + */ + @Override + public Slice getSlice() { + // Get current storage percentage from StorageManager. + final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo( + new StorageManagerVolumeProvider(mContext.getSystemService(StorageManager.class))); + final double currentStoragePercentage = + (double) (info.totalBytes - info.freeBytes) / info.totalBytes; + + // Used storage < 85%. NOT show Low storage Slice. + if (currentStoragePercentage < LOW_STORAGE_THRESHOLD) { + /** + * TODO(b/114808204): Contextual Home Page - "Low Storage" + * The behavior is under decision making, will update new behavior or remove TODO later. + */ + Log.i(TAG, "Not show low storage slice, not match condition."); + return null; + } + + // Show Low storage Slice. + final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_storage); + final CharSequence title = mContext.getText(R.string.storage_menu_free); + final SliceAction primarySliceAction = new SliceAction( + PendingIntent.getActivity(mContext, 0, getIntent(), 0), icon, title); + final String lowStorageSummary = mContext.getString(R.string.low_storage_summary, + NumberFormat.getPercentInstance().format(currentStoragePercentage), + Formatter.formatFileSize(mContext, info.freeBytes)); + + /** + * TODO(b/114808204): Contextual Home Page - "Low Storage" + * Slices doesn't support "Icon on the left" in header. Now we intend to start with Icon + * right aligned. Will update the icon to left until Slices support it. + */ + return new ListBuilder(mContext, LOW_STORAGE_URI, ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .addRow(new RowBuilder() + .setTitle(title) + .setSubtitle(lowStorageSummary) + .addEndItem(icon, ListBuilder.ICON_IMAGE) + .setPrimaryAction(primarySliceAction)) + .build(); + } + + @Override + public Uri getUri() { + return LOW_STORAGE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.storage_label) + .toString(); + final Uri contentUri = new Uri.Builder().appendPath(PATH_LOW_STORAGE).build(); + + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + StorageSettings.class.getName(), PATH_LOW_STORAGE, + screenTitle, + MetricsProto.MetricsEvent.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + } +} \ 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 556c69817b3..5b2549800c8 100644 --- a/src/com/android/settings/slices/CustomSliceManager.java +++ b/src/com/android/settings/slices/CustomSliceManager.java @@ -25,6 +25,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; +import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; import com.android.settings.wifi.WifiSlice; import java.util.Map; @@ -105,5 +106,6 @@ public class CustomSliceManager { mUriMap.put(StorageSlice.STORAGE_CARD_URI, StorageSlice.class); mUriMap.put(BatterySlice.BATTERY_CARD_URI, BatterySlice.class); mUriMap.put(ConnectedDeviceSlice.CONNECTED_DEVICE_URI, ConnectedDeviceSlice.class); + mUriMap.put(LowStorageSlice.LOW_STORAGE_URI, LowStorageSlice.class); } } diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/LowStorageSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/LowStorageSliceTest.java new file mode 100644 index 00000000000..0be55d928f6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/LowStorageSliceTest.java @@ -0,0 +1,109 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceProvider; +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 com.android.settingslib.deviceinfo.StorageVolumeProvider; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +public class LowStorageSliceTest { + + private Context mContext; + private LowStorageSlice mLowStorageSlice; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mLowStorageSlice = new LowStorageSlice(mContext); + } + + @After + public void tearDown() { + ShadowPrivateStorageInfo.reset(); + } + + @Test + @Config(shadows = ShadowPrivateStorageInfo.class) + public void getSlice_hasLowStorage_shouldBeCorrectSliceContent() { + ShadowPrivateStorageInfo.setPrivateStorageInfo(new PrivateStorageInfo(10L, 100L)); + + final Slice slice = mLowStorageSlice.getSlice(); + + final List sliceItems = slice.getItems(); + SliceTester.assertTitle(sliceItems, mContext.getString(R.string.storage_menu_free)); + } + + @Test + @Config(shadows = ShadowPrivateStorageInfo.class) + public void getSlice_hasNoLowStorage_shouldBeNull() { + ShadowPrivateStorageInfo.setPrivateStorageInfo(new PrivateStorageInfo(100L, 100L)); + + final Slice slice = mLowStorageSlice.getSlice(); + + assertThat(slice).isNull(); + } + + @Implements(PrivateStorageInfo.class) + public static class ShadowPrivateStorageInfo { + + private static PrivateStorageInfo sPrivateStorageInfo = null; + + @Resetter + public static void reset() { + sPrivateStorageInfo = null; + } + + @Implementation + public static PrivateStorageInfo getPrivateStorageInfo( + StorageVolumeProvider storageVolumeProvider) { + return sPrivateStorageInfo; + } + + public static void setPrivateStorageInfo( + PrivateStorageInfo privateStorageInfo) { + sPrivateStorageInfo = privateStorageInfo; + } + } +} \ No newline at end of file