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