Merge "Parse external intent from cursor correctly for search."
diff --git a/res/layout/storage_summary_donut.xml b/res/layout/storage_summary_donut.xml
index 9cffe69..d9ead9c 100644
--- a/res/layout/storage_summary_donut.xml
+++ b/res/layout/storage_summary_donut.xml
@@ -14,47 +14,61 @@
      limitations under the License.
 -->
 
-<!-- TODO: Update this view to not match the existing storage summary.-->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
-              android:orientation="vertical"
-              android:minHeight="?android:attr/listPreferredItemHeightSmall"
+              android:orientation="horizontal"
               android:gravity="center_vertical"
-              android:paddingStart="@dimen/preference_no_icon_padding_start"
-              android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-              android:paddingTop="16dip"
-              android:paddingBottom="16dip"
               android:background="?android:attr/selectableItemBackground">
 
-    <TextView
-        android:id="@android:id/title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:singleLine="true"
-        android:textAlignment="viewStart"
-        android:textAppearance="@android:style/TextAppearance.Material.Subhead"
-        android:textColor="?android:attr/colorAccent"
-        android:textSize="36sp"
-        android:ellipsize="marquee"
-        android:fadingEdge="horizontal" />
+    <LinearLayout android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:orientation="vertical"
+                  android:minHeight="?android:attr/listPreferredItemHeightSmall"
+                  android:gravity="center_vertical"
+                  android:paddingStart="@dimen/preference_no_icon_padding_start"
+                  android:paddingEnd="@dimen/storage_summary_padding_end"
+                  android:paddingTop="16dip"
+                  android:paddingBottom="16dip"
+                  android:enabled="false">
 
-    <TextView
-        android:id="@android:id/summary"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textAlignment="viewStart"
-        android:textAppearance="@android:style/TextAppearance.Material.Body1"
-        android:maxLines="10" />
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAlignment="viewStart"
+            android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+            android:textColor="?android:attr/colorAccent"
+            android:textSize="36sp"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal" />
 
-    <ProgressBar
-        android:id="@android:id/progress"
-        android:layout_width="match_parent"
-        android:layout_height="8dp"
-        android:layout_marginTop="16dp"
-        android:layout_marginBottom="8dp"
-        android:visibility="gone"
-        android:max="100"
-        style="?android:attr/progressBarStyleHorizontal" />
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_marginStart="4dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            android:textAppearance="@android:style/TextAppearance.Material.Body1"
+            android:maxLines="10" />
+
+        <Button
+            android:id="@+id/deletion_helper_button"
+            android:theme="@style/FreeUpStorageButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/summary"
+            android:text="@string/storage_menu_free"/>
+    </LinearLayout>
+
+    <com.android.settings.widget.DonutView
+        android:id="@+id/donut"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        android:minWidth="58dip"
+        android:gravity="end|center_vertical"/>
 
 </LinearLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a8d35dc..38eb74b 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -111,6 +111,9 @@
     <!-- Gestures settings -->
     <color name="gestures_setting_background_color">#f5f5f5</color>
 
-    <!-- TODO: revert it after the SettingsShadowResources is globally finalized -->
     <color name="status_bar_color">#3c3c3c</color>
+
+    <!-- Color for the background of the donut graph.-->
+    <color name="donut_background_grey">#ffd7d7d7</color>
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 4b3dba9..bc159d6 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -307,4 +307,7 @@
     <!-- Padding for the escalation card in normal dimens -->
     <dimen name="support_escalation_card_padding_start">40dp</dimen>
     <dimen name="support_escalation_card_padding_end">40dp</dimen>
+
+    <!-- Padding between the donut and the storage summary. -->
+    <dimen name="storage_summary_padding_end">16dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b9e9c1c..73c7e3b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8019,4 +8019,14 @@
     <!-- Preference label for the Files storage section. [CHAR LIMIT=50] -->
     <string name="storage_files">Files</string>
 
+    <!-- Main settings screen item's title to go into the storage settings screen [CHAR LIMIT=25] -->
+    <string name="storage_settings_2" >Phone Storage</string>
+
+    <!-- Summary of a single storage volume used space. [CHAR LIMIT=24] -->
+    <string name="storage_size_large_alternate"><xliff:g id="number" example="128">^1</xliff:g><small><small> <xliff:g id="unit" example="KB">^2</xliff:g> used</small></small></string>
+    <!-- Summary of a single storage volume free space. [CHAR LIMIT=48]-->
+    <string name="storage_volume_free"><xliff:g id="total" example="32GB">%1$s</xliff:g> free</string>
+    <!-- The percent of storage used by a storage volume. Exposed inside of a donut graph. [CHAR LIMIT=4]-->
+    <string name="storage_percent_used"><xliff:g id="percent" example="50%">%1$s</xliff:g>%%</string>
+
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index a3baa21..a4a679b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -437,4 +437,10 @@
     </style>
 
     <style name="AppActionPrimaryButton" parent="android:Widget.Material.Button.Colored"/>
+
+    <style name="FreeUpStorageButton">
+        <item name="android:buttonStyle">@android:style/Widget.Material.Button</item>
+        <item name="android:colorButtonNormal">#fff</item>
+    </style>
+
 </resources>
diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
index abefce6..c9572b3 100644
--- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
+++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
@@ -46,7 +46,6 @@
     private static final String TAG = "StorageDashboardFrag";
 
     private VolumeInfo mVolume;
-    private long mTotalSize;
 
     private StorageSummaryDonutPreferenceController mSummaryController;
     private StorageItemPreferenceController mPreferenceController;
@@ -70,17 +69,19 @@
         }
 
         final long sharedDataSize = mVolume.getPath().getTotalSpace();
-        mTotalSize = sm.getPrimaryStorageSize();
-        long systemSize = mTotalSize - sharedDataSize;
+        long totalSize = sm.getPrimaryStorageSize();
+        long systemSize = totalSize - sharedDataSize;
 
-        if (mTotalSize <= 0) {
-            mTotalSize = sharedDataSize;
+        if (totalSize <= 0) {
+            totalSize = sharedDataSize;
             systemSize = 0;
         }
 
-        final long usedBytes = mTotalSize - mVolume.getPath().getFreeSpace();
-        mSummaryController.updateBytes(usedBytes, mTotalSize);
+        final long usedBytes = totalSize - mVolume.getPath().getFreeSpace();
+        mSummaryController.updateBytes(usedBytes, totalSize);
         mPreferenceController.setVolume(mVolume);
+        mPreferenceController.setSystemSize(systemSize);
+        mPreferenceController.startMeasurement();
 
         // Initialize the footer preference to go to the smart storage management.
         final FooterPreference pref = mFooterPreferenceMixin.createFooterPreference();
@@ -88,8 +89,6 @@
         pref.setFragment("com.android.settings.deletionhelper.AutomaticStorageManagerSettings");
         pref.setIcon(R.drawable.ic_settings_storage);
         pref.setEnabled(true);
-
-
     }
 
     @Override
@@ -119,20 +118,13 @@
         controllers.add(mSummaryController);
 
         StorageManager sm = context.getSystemService(StorageManager.class);
-        mPreferenceController = new StorageItemPreferenceController(context, this, mVolume,
-                new StorageManagerVolumeProvider(sm));
+        mPreferenceController = new StorageItemPreferenceController(context, getLifecycle(), this,
+                mVolume, new StorageManagerVolumeProvider(sm));
         controllers.add(mPreferenceController);
         controllers.add(new ManageStoragePreferenceController(context));
         return controllers;
     }
 
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        View root = super.onCreateView(inflater, container, savedInstanceState);
-        // TODO: Add loader to load the storage sizes for the StorageItemPreferenceControllers.
-        return root;
-    }
     /**
      * For Search.
      */
diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
index 3e802fd..dce8a25 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
@@ -21,11 +21,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.VolumeInfo;
 import android.provider.DocumentsContract;
+import android.support.annotation.VisibleForTesting;
 import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
 import android.util.Log;
 
 import com.android.settings.R;
@@ -33,24 +36,56 @@
 import com.android.settings.Utils;
 import com.android.settings.applications.ManageApplications;
 import com.android.settings.core.PreferenceController;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.core.lifecycle.LifecycleObserver;
+import com.android.settings.core.lifecycle.events.OnDestroy;
+import com.android.settings.deviceinfo.StorageItemPreference;
+import com.android.settingslib.deviceinfo.StorageMeasurement;
 import com.android.settingslib.deviceinfo.StorageVolumeProvider;
 
+import java.util.HashMap;
+
 
 /**
  * StorageItemPreferenceController handles the storage line items which summarize the storage
  * categorization breakdown.
  */
-public class StorageItemPreferenceController extends PreferenceController {
+public class StorageItemPreferenceController extends PreferenceController
+        implements StorageMeasurement.MeasurementReceiver, LifecycleObserver, OnDestroy {
     private static final String TAG = "StorageItemPreference";
+
+    @VisibleForTesting
+    static final String PHOTO_KEY = "pref_photos_videos";
+    @VisibleForTesting
+    static final String AUDIO_KEY = "pref_music_audio";
+    @VisibleForTesting
+    static final String GAME_KEY = "pref_games";
+    @VisibleForTesting
+    static final String OTHER_APPS_KEY = "pref_other_apps";
+    @VisibleForTesting
+    static final String SYSTEM_KEY = "pref_system";
+    @VisibleForTesting
+    static final String FILES_KEY = "pref_files";
+
     private final Fragment mFragment;
     private final StorageVolumeProvider mSvp;
     private VolumeInfo mVolume;
     private final int mUserId;
+    private StorageMeasurement mMeasure;
+    private long mSystemSize;
+    private long mUsedSize;
+
+    private StorageItemPreferenceAlternate mPhotoPreference;
+    private StorageItemPreferenceAlternate mAudioPreference;
+    private StorageItemPreferenceAlternate mGamePreference;
+    private StorageItemPreferenceAlternate mAppPreference;
+    private StorageItemPreferenceAlternate mFilePreference;
+    private StorageItemPreferenceAlternate mSystemPreference;
 
     private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
 
-    public StorageItemPreferenceController(Context context, Fragment hostFragment,
-            VolumeInfo volume, StorageVolumeProvider svp) {
+    public StorageItemPreferenceController(Context context, Lifecycle lifecycle,
+            Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp) {
         super(context);
         mFragment = hostFragment;
         mVolume = volume;
@@ -58,6 +93,10 @@
 
         UserManager um = mContext.getSystemService(UserManager.class);
         mUserId = um.getUserHandle();
+
+        if (lifecycle != null) {
+            lifecycle.addObserver(this);
+        }
     }
 
     @Override
@@ -75,15 +114,15 @@
         //       After the intermediate views are built, swap them in.
         Intent intent = null;
         switch (preference.getKey()) {
-            case "pref_photos_videos":
+            case PHOTO_KEY:
                 intent = getPhotosIntent();
                 break;
-            case "pref_music_audio":
+            case AUDIO_KEY:
                 intent = getAudioIntent();
                 break;
-            case "pref_games":
+            case GAME_KEY:
                 // TODO: Once app categorization is added, make this section.
-            case "pref_other_apps":
+            case OTHER_APPS_KEY:
                 // Because we are likely constructed with a null volume, this is theoretically
                 // possible.
                 if (mVolume == null) {
@@ -91,7 +130,7 @@
                 }
                 intent = getAppsIntent();
                 break;
-            case "pref_files":
+            case FILES_KEY:
                 intent = getFilesIntent();
                 break;
         }
@@ -118,6 +157,81 @@
         mVolume = volume;
     }
 
+    @Override
+    public void onDetailsChanged(StorageMeasurement.MeasurementDetails details) {
+        final long imagesSize = totalValues(details, mUserId,
+                Environment.DIRECTORY_DCIM,
+                Environment.DIRECTORY_PICTURES,
+                Environment.DIRECTORY_MOVIES);
+        if (mPhotoPreference != null) {
+            mPhotoPreference.setStorageSize(imagesSize);
+        }
+
+        final long audioSize = totalValues(details, mUserId,
+                Environment.DIRECTORY_MUSIC,
+                Environment.DIRECTORY_ALARMS,
+                Environment.DIRECTORY_NOTIFICATIONS,
+                Environment.DIRECTORY_RINGTONES,
+                Environment.DIRECTORY_PODCASTS);
+        if (mAudioPreference != null) {
+            mAudioPreference.setStorageSize(audioSize);
+        }
+
+        if (mGamePreference != null) {
+            mGamePreference.setStorageSize(0);
+        }
+
+        final long appSize = details.appsSize.get(mUserId);
+        if (mAppPreference != null) {
+            mAppPreference.setStorageSize(appSize);
+        }
+
+        if (mSystemPreference != null) {
+            mSystemPreference.setStorageSize(mSystemSize);
+        }
+
+        final long downloadsSize = totalValues(details, mUserId, Environment.DIRECTORY_DOWNLOADS);
+        final long miscSize = details.miscSize.get(mUserId);
+        if (mFilePreference != null) {
+            mFilePreference.setStorageSize(downloadsSize + miscSize);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mMeasure != null) {
+            mMeasure.onDestroy();
+        }
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        mPhotoPreference = (StorageItemPreferenceAlternate) screen.findPreference(PHOTO_KEY);
+        mAudioPreference = (StorageItemPreferenceAlternate) screen.findPreference(AUDIO_KEY);
+        mGamePreference = (StorageItemPreferenceAlternate) screen.findPreference(GAME_KEY);
+        mAppPreference = (StorageItemPreferenceAlternate) screen.findPreference(OTHER_APPS_KEY);
+        mSystemPreference = (StorageItemPreferenceAlternate) screen.findPreference(SYSTEM_KEY);
+        mFilePreference = (StorageItemPreferenceAlternate) screen.findPreference(FILES_KEY);
+    }
+
+    /**
+     * Begins an asynchronous storage measurement task for the preferences.
+     */
+    public void startMeasurement() {
+        //TODO: When the GID-based measurement system is completed, swap in the GID impl.
+        mMeasure = new StorageMeasurement(mContext, mVolume, mSvp.findEmulatedForPrivate(mVolume));
+        mMeasure.setReceiver(this);
+        mMeasure.forceMeasure();
+    }
+
+    /**
+     * Sets the system size for the system size preference.
+     * @param systemSize the size of the system in bytes
+     */
+    public void setSystemSize(long systemSize) {
+        mSystemSize = systemSize;
+    }
+
     private Intent getPhotosIntent() {
         Intent intent = new Intent(DocumentsContract.ACTION_BROWSE);
         intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "images_root"));
@@ -160,4 +274,20 @@
             Log.w(TAG, "No activity found for " + intent);
         }
     }
+
+    private static long totalValues(StorageMeasurement.MeasurementDetails details, int userId,
+            String... keys) {
+        long total = 0;
+        HashMap<String, Long> map = details.mediaSize.get(userId);
+        if (map != null) {
+            for (String key : keys) {
+                if (map.containsKey(key)) {
+                    total += map.get(key);
+                }
+            }
+        } else {
+            Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
+        }
+        return total;
+    }
 }
diff --git a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java
index d6fd354..9e39034 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java
@@ -17,20 +17,23 @@
 package com.android.settings.deviceinfo.storage;
 
 import android.content.Context;
+import android.content.Intent;
+import android.os.storage.StorageManager;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceViewHolder;
 import android.util.AttributeSet;
 import android.util.MathUtils;
 import android.view.View;
-import android.widget.ProgressBar;
+import android.widget.Button;
 
 import com.android.settings.R;
+import com.android.settings.widget.DonutView;
 
 /**
  * StorageSummaryDonutPreference is a preference which summarizes the used and remaining storage left
  * on a given storage volume. It is visualized with a donut graphing the % used.
  */
-public class StorageSummaryDonutPreference extends Preference {
+public class StorageSummaryDonutPreference extends Preference implements View.OnClickListener {
     private int mPercent = -1;
 
     public StorageSummaryDonutPreference(Context context) {
@@ -55,17 +58,23 @@
 
     @Override
     public void onBindViewHolder(PreferenceViewHolder view) {
-        // TODO: Replace the progress bar with a donut.
-        final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress);
-        if (mPercent != -1) {
-            progress.setVisibility(View.VISIBLE);
-            progress.setProgress(mPercent);
-            progress.setScaleY(7f);
-        } else {
-            progress.setVisibility(View.GONE);
+        super.onBindViewHolder(view);
+        final DonutView donut = (DonutView) view.findViewById(R.id.donut);
+        if (donut != null) {
+            donut.setPercentage(mPercent);
         }
 
-        super.onBindViewHolder(view);
+        final Button deletionHelperButton = (Button) view.findViewById(R.id.deletion_helper_button);
+        if (deletionHelperButton != null) {
+            deletionHelperButton.setOnClickListener(this);
+        }
     }
 
+    @Override
+    public void onClick(View v) {
+        if (v != null && R.id.deletion_helper_button == v.getId()) {
+            Intent intent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
+            getContext().startActivity(intent);
+        }
+    }
 }
diff --git a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java
index 1d2478a..a962669 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java
@@ -1,16 +1,33 @@
+/*
+ * Copyright (C) 2017 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.deviceinfo.storage;
 
 import android.content.Context;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
 import android.support.v7.preference.Preference;
-import android.support.v7.preference.PreferenceGroup;
 import android.support.v7.preference.PreferenceScreen;
 import android.text.TextUtils;
 import android.text.format.Formatter;
-import android.util.Log;
-import android.widget.TextView;
 
-import com.android.settings.core.PreferenceController;
 import com.android.settings.R;
+import com.android.settings.core.PreferenceController;
+import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
+import com.android.settingslib.deviceinfo.StorageVolumeProvider;
 
 /**
  * StorgaeSummaryPreferenceController updates the donut storage summary preference to have the
@@ -37,12 +54,13 @@
         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),
-                result.value, result.units));
-        summary.setSummary(mContext.getString(R.string.storage_volume_used,
-                Formatter.formatFileSize(mContext, mTotalBytes)));
-        summary.setEnabled(true);
+        summary.setTitle(TextUtils.expandTemplate(
+                mContext.getText(R.string.storage_size_large_alternate), result.value,
+                result.units));
+        summary.setSummary(mContext.getString(R.string.storage_volume_free,
+                Formatter.formatFileSize(mContext, mTotalBytes - mUsedBytes)));
         summary.setPercent(mUsedBytes, mTotalBytes);
+        summary.setEnabled(true);
     }
 
     @Override
@@ -51,11 +69,6 @@
     }
 
     @Override
-    public boolean handlePreferenceTreeClick(Preference preference) {
-        return false;
-    }
-
-    @Override
     public String getPreferenceKey() {
         return "pref_summary";
     }
@@ -69,4 +82,20 @@
         mUsedBytes = used;
         mTotalBytes = total;
     }
+
+    /**
+     * 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) {
+        final long sharedDataSize = volume.getPath().getTotalSpace();
+        long totalSize = svp.getPrimaryStorageSize();
+
+        if (totalSize <= 0) {
+            totalSize = sharedDataSize;
+        }
+
+        final long usedBytes = totalSize - volume.getPath().getFreeSpace();
+        updateBytes(usedBytes, totalSize);
+    }
 }
diff --git a/src/com/android/settings/widget/DonutView.java b/src/com/android/settings/widget/DonutView.java
new file mode 100644
index 0000000..845f52c
--- /dev/null
+++ b/src/com/android/settings/widget/DonutView.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+
+/**
+ * DonutView represents a donut graph. It visualizes a certain percentage of fullness with a
+ * corresponding label with the fullness on the inside (i.e. "50%" inside of the donut.
+ */
+public class DonutView extends View {
+    private static final int TOP = -90;
+    private float mStrokeWidth;
+    private int mPercent;
+    private Paint mBackgroundCircle;
+    private Paint mFilledArc;
+    private TextPaint mTextPaint;
+
+    public DonutView(Context context) {
+        super(context);
+    }
+
+    public DonutView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        float density = getResources().getDisplayMetrics().density;
+        mStrokeWidth = 10f * density;
+
+        mBackgroundCircle = new Paint();
+        mBackgroundCircle.setAntiAlias(true);
+        mBackgroundCircle.setStrokeCap(Paint.Cap.BUTT);
+        mBackgroundCircle.setStyle(Paint.Style.STROKE);
+        mBackgroundCircle.setStrokeWidth(mStrokeWidth);
+        mBackgroundCircle.setColor(getResources().getColor(R.color.donut_background_grey));
+
+        mFilledArc = new Paint();
+        mFilledArc.setAntiAlias(true);
+        mFilledArc.setStrokeCap(Paint.Cap.BUTT);
+        mFilledArc.setStyle(Paint.Style.STROKE);
+        mFilledArc.setStrokeWidth(mStrokeWidth);
+        mFilledArc.setColor(Utils.getColorAccent(getContext()));
+
+        mTextPaint = new TextPaint();
+        mTextPaint.setColor(Utils.getColorAccent(getContext()));
+        mTextPaint.setAntiAlias(true);
+        mTextPaint.setTextSize(18f * density);
+        mTextPaint.setTextAlign(Paint.Align.CENTER);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawArc(0 + mStrokeWidth, 0 + mStrokeWidth, getWidth() - mStrokeWidth,
+                getHeight() - mStrokeWidth, TOP, 360, false, mBackgroundCircle);
+
+        canvas.drawArc(0 + mStrokeWidth, 0 + mStrokeWidth, getWidth() - mStrokeWidth,
+                getHeight() - mStrokeWidth, TOP, (360 * mPercent / 100), false, mFilledArc);
+
+        int centerX = getWidth() / 2;
+        int centerY = getHeight() / 2;
+
+        String percentString =
+                String.format(getContext().getString(R.string.storage_percent_used), mPercent);
+        // drawText uses the Y dimension as the floor of the text, so we do this to center.
+        canvas.drawText(percentString, centerX,
+                centerY + getTextHeight(mTextPaint) / 2 - mTextPaint.descent(),
+                mTextPaint);
+    }
+
+    /**
+     * Set a percentage full to have the donut graph.
+     */
+    public void setPercentage(int percent) {
+        mPercent = percent;
+        invalidate();
+    }
+
+    private float getTextHeight(TextPaint paint) {
+        // Technically, this should be the cap height, but I can live with the descent - ascent.
+        return paint.descent() - paint.ascent();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java
index 19936c4..b0c0b44 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java
@@ -18,16 +18,20 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Fragment;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Environment;
 import android.os.UserHandle;
 import android.os.storage.VolumeInfo;
 import android.provider.DocumentsContract;
+import android.support.v7.preference.PreferenceScreen;
 import android.view.LayoutInflater;
+import android.view.View;
 import android.widget.LinearLayout;
 
 import com.android.settings.R;
@@ -36,6 +40,7 @@
 import com.android.settings.SubSettings;
 import com.android.settings.TestConfig;
 import com.android.settings.applications.ManageApplications;
+import com.android.settingslib.deviceinfo.StorageMeasurement;
 import com.android.settingslib.deviceinfo.StorageVolumeProvider;
 
 import org.junit.Before;
@@ -44,13 +49,22 @@
 import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
+import java.util.HashMap;
+
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class StorageItemPreferenceControllerTest {
+    /**
+     *  In O, this will change to 1000 instead of 1024 due to the formatter properly defining a
+     *  kilobyte.
+     */
+    private static long KILOBYTE = 1024L;
+
     private Context mContext;
     private VolumeInfo mVolume;
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -65,12 +79,15 @@
         MockitoAnnotations.initMocks(this);
         mVolume = new VolumeInfo("id", 0, null, "id");
         mContext = RuntimeEnvironment.application;
-        mController = new StorageItemPreferenceController(mContext, mFragment, mVolume, mSvp);
+        // Note: null is passed as the Lifecycle because we are handling it outside of the normal
+        //       Settings fragment lifecycle for test purposes.
+        mController = new StorageItemPreferenceController(mContext, null, mFragment, mVolume, mSvp);
         mPreference = new StorageItemPreferenceAlternate(mContext);
 
         // Inflate the preference and the widget.
         LayoutInflater inflater = LayoutInflater.from(mContext);
-        inflater.inflate(mPreference.getLayoutResource(), new LinearLayout(mContext), false);
+        final View view = inflater.inflate(
+                mPreference.getLayoutResource(), new LinearLayout(mContext), false);
     }
 
     @Test
@@ -142,4 +159,46 @@
         assertThat(intent.getAction()).isEqualTo(browseIntent.getAction());
         assertThat(intent.getData()).isEqualTo(browseIntent.getData());
     }
+
+    @Test
+    public void testMeasurementCompletedUpdatesPreferences() {
+        StorageItemPreferenceAlternate audio = new StorageItemPreferenceAlternate(mContext);
+        StorageItemPreferenceAlternate image = new StorageItemPreferenceAlternate(mContext);
+        StorageItemPreferenceAlternate games = new StorageItemPreferenceAlternate(mContext);
+        StorageItemPreferenceAlternate apps = new StorageItemPreferenceAlternate(mContext);
+        StorageItemPreferenceAlternate system = new StorageItemPreferenceAlternate(mContext);
+        StorageItemPreferenceAlternate files = new StorageItemPreferenceAlternate(mContext);
+        PreferenceScreen screen = mock(PreferenceScreen.class);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.AUDIO_KEY))).thenReturn(audio);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.PHOTO_KEY))).thenReturn(image);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.GAME_KEY))).thenReturn(games);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.OTHER_APPS_KEY))).thenReturn(apps);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.SYSTEM_KEY))).thenReturn(system);
+        when(screen.findPreference(
+                Mockito.eq(StorageItemPreferenceController.FILES_KEY))).thenReturn(files);
+        mController.displayPreference(screen);
+
+        StorageMeasurement.MeasurementDetails details = new StorageMeasurement.MeasurementDetails();
+        details.appsSize.put(0, KILOBYTE);
+        HashMap<String, Long> mediaSizes = new HashMap<>();
+        mediaSizes.put(Environment.DIRECTORY_PICTURES, KILOBYTE * 2);
+        mediaSizes.put(Environment.DIRECTORY_MOVIES, KILOBYTE * 3);
+        mediaSizes.put(Environment.DIRECTORY_MUSIC, KILOBYTE * 4);
+        mediaSizes.put(Environment.DIRECTORY_DOWNLOADS, KILOBYTE * 5);
+        details.mediaSize.put(0, mediaSizes);
+        mController.setSystemSize(KILOBYTE * 6);
+        mController.onDetailsChanged(details);
+
+        assertThat(audio.getSummary().toString()).isEqualTo("4.00KB");
+        assertThat(image.getSummary().toString()).isEqualTo("5.00KB");
+        assertThat(games.getSummary().toString()).isEqualTo("0");
+        assertThat(apps.getSummary().toString()).isEqualTo("1.00KB");
+        assertThat(system.getSummary().toString()).isEqualTo("6.00KB");
+        assertThat(files.getSummary().toString()).isEqualTo("5.00KB");
+    }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceControllerTest.java
new file mode 100644
index 0000000..848616a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceControllerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.deviceinfo.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.storage.VolumeInfo;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settingslib.deviceinfo.StorageVolumeProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.io.File;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class StorageSummaryDonutPreferenceControllerTest {
+    private static String KEY = "pref";
+
+    private Context mContext;
+    private StorageSummaryDonutPreferenceController mController;
+    private StorageSummaryDonutPreference mPreference;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = RuntimeEnvironment.application;
+        mController = new StorageSummaryDonutPreferenceController(mContext);
+        mPreference = new StorageSummaryDonutPreference(mContext);
+
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        inflater.inflate(mPreference.getLayoutResource(), new LinearLayout(mContext), false);
+    }
+
+    @Test
+    public void testEmpty() throws Exception {
+        mController.updateBytes(0, 0);
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.getTitle().toString()).isEqualTo("0.00B used");
+        assertThat(mPreference.getSummary().toString()).isEqualTo("0.00B free");
+    }
+
+    @Test
+    public void testUsedStorage() throws Exception {
+        mController.updateBytes(1024, 1024 * 10);
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.getTitle().toString()).isEqualTo("1.00KB used");
+        assertThat(mPreference.getSummary().toString()).isEqualTo("9.00KB free");
+    }
+
+    @Test
+    public void testPopulateWithVolume() throws Exception {
+        VolumeInfo volume = Mockito.mock(VolumeInfo.class);
+        File file = Mockito.mock(File.class);
+        StorageVolumeProvider svp = Mockito.mock(StorageVolumeProvider.class);
+        when(volume.getPath()).thenReturn(file);
+        when(file.getTotalSpace()).thenReturn(1024L * 10);
+        when(file.getFreeSpace()).thenReturn(1024L);
+        when(svp.getPrimaryStorageSize()).thenReturn(1024L * 10);
+
+        mController.updateSizes(svp, volume);
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.getTitle().toString()).isEqualTo("9.00KB used");
+        assertThat(mPreference.getSummary().toString()).isEqualTo("1.00KB free");
+    }
+}