Add format progress and slow drive warning

Add SettingsAsyncTaskLoader (copied from Email) to manage async tasks as a loader
Add progress dialog for formatting as private
Add warning dialog for slow storage
Move a bunch of wizard logic into StorageResetActivity
Fix a string length limit

b/21172095
b/21193723
b/21328809

Change-Id: I883eba4f0d380853bd7705ab6969b90ea6808d14
diff --git a/Settings/res/values/strings.xml b/Settings/res/values/strings.xml
index cfbe8be..573bfba 100644
--- a/Settings/res/values/strings.xml
+++ b/Settings/res/values/strings.xml
@@ -478,13 +478,24 @@
     <!-- Title for wizard screen for formatting storage as internal [CHAR LIMIT=64] -->
     <string name="storage_wizard_format_as_private_title">Format as internal storage</string>
     <!-- Description for wizard screen for formatting storage as internal [CHAR LIMIT=NONE] -->
-    <string name="storage_wizard_format__as_private_description">This requires the USB drive to be formatted to make it secure. After securely formatting, this drive will only work with this device. Formatting erases all data currently stored on the drive. To avoid losing the data, consider backing it up.</string>
+    <string name="storage_wizard_format_as_private_description">This requires the USB drive to be formatted to make it secure. After securely formatting, this drive will only work with this device. Formatting erases all data currently stored on the drive. To avoid losing the data, consider backing it up.</string>
 
     <!-- Title for wizard screen for formatting storage as internal [CHAR LIMIT=64] -->
     <string name="storage_wizard_format_as_public_title">Erase &amp; Format</string>
     <!-- Description for wizard screen for formatting storage as internal [CHAR LIMIT=NONE] -->
     <string name="storage_wizard_format_as_public_description">After formatting, you can use this USB drive in other devices. All data will be erased. Consider backing up first by moving apps to other internal storage.</string>
 
+    <!-- Title for wizard progress screen for formatting drive [CHAR_LIMIT=50] -->
+    <string name="storage_wizard_format_progress_title">Formatting USB Drive&#8230;</string>
+    <!-- Description for wizard progress screen for formatting drive [CHAR_LIMIT=NONE] -->
+    <string name="storage_wizard_format_progress_description">This may take a moment. Please don\'t remove the drive.</string>
+
+    <!-- Title for warning dialog for slow drives [CHAR_LIMIT=64] -->
+    <string name="storage_wizard_format_slow_title">This drive appears to be slow.</string>
+
+    <!-- Summary for warning dialog for slow drives [CHAR_LIMIT=NONE] -->
+    <string name="storage_wizard_format_slow_summary">You can continue, but apps moved to this location may stutter and data transfers may take a long time. Consider using a faster drive for better performance.</string>
+
     <!-- Format action for wizard screen for formatting storage [CHAR LIMIT=50] -->
     <string name="storage_wizard_format_action">Format</string>
     <!-- Back up apps action for wizard screen for formatting storage [CHAR LIMIT=50]-->
@@ -507,7 +518,7 @@
 
     <!-- Title for wizard progress screen for moving apps [CHAR_LIMIT=50] -->
     <string name="storage_wizard_move_app_progress_title">Moving <xliff:g id="name" example="YouTube">%1$s</xliff:g>&#8230;</string>
-    <!-- Description for wizard progress screen for moving apps [CHAR_LIMIT=50] -->
+    <!-- Description for wizard progress screen for moving apps [CHAR_LIMIT=NONE] -->
     <string name="storage_wizard_move_app_progress_description">"Don't remove the drive during the move.\nThe <xliff:g id="appname" example="YouTube">%1$s</xliff:g> app on this device won't be available until the move is complete.</string>
 
     <!-- Manage applications, text for move error messages -->
diff --git a/Settings/src/com/android/tv/settings/device/StorageResetActivity.java b/Settings/src/com/android/tv/settings/device/StorageResetActivity.java
index 96a8004..2cc4255 100644
--- a/Settings/src/com/android/tv/settings/device/StorageResetActivity.java
+++ b/Settings/src/com/android/tv/settings/device/StorageResetActivity.java
@@ -22,9 +22,11 @@
 import android.app.DialogFragment;
 import android.app.Fragment;
 import android.app.FragmentManager;
+import android.app.LoaderManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.Loader;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -37,6 +39,7 @@
 import android.os.storage.StorageManager;
 import android.os.storage.VolumeInfo;
 import android.support.annotation.NonNull;
+import android.text.TextUtils;
 import android.text.format.Formatter;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -50,14 +53,17 @@
 import com.android.tv.settings.device.storage.EjectInternalStepFragment;
 import com.android.tv.settings.device.storage.FormatAsPublicStepFragment;
 import com.android.tv.settings.device.storage.FormatAsPrivateStepFragment;
+import com.android.tv.settings.device.storage.FormattingProgressFragment;
 import com.android.tv.settings.device.storage.MoveAppProgressFragment;
 import com.android.tv.settings.device.storage.MoveAppStepFragment;
+import com.android.tv.settings.device.storage.SlowDriveStepFragment;
 import com.android.tv.settings.dialog.Layout;
 import com.android.tv.settings.dialog.Layout.Action;
 import com.android.tv.settings.dialog.Layout.Header;
 import com.android.tv.settings.dialog.Layout.Status;
 import com.android.tv.settings.dialog.Layout.StringGetter;
 import com.android.tv.settings.dialog.SettingsLayoutActivity;
+import com.android.tv.settings.util.SettingsAsyncTaskLoader;
 
 import java.io.File;
 import java.util.Collections;
@@ -69,7 +75,7 @@
  * Activity to view storage consumption and factory reset device.
  */
 public class StorageResetActivity extends SettingsLayoutActivity
-        implements MoveAppStepFragment.Callback {
+        implements MoveAppStepFragment.Callback, FormatAsPrivateStepFragment.Callback {
 
     private static final String TAG = "StorageResetActivity";
     private static final long INVALID_SIZE = -1;
@@ -89,9 +95,14 @@
      */
     private static final String SHUTDOWN_INTENT_EXTRA = "shutdown";
 
-    private static final String PROGRESS_DIALOG_BACKSTACK_TAG = "progressDialog";
+    private static final String MOVE_PROGRESS_DIALOG_BACKSTACK_TAG = "moveProgressDialog";
+    private static final String FORMAT_DIALOG_BACKSTACK_TAG = "formatDialog";
 
     private static final String SAVE_STATE_MOVE_ID = "StorageResetActivity.moveId";
+    private static final String SAVE_STATE_FORMAT_PRIVATE_DISK_ID =
+            "StorageResetActivity.formatPrivateDiskId";
+    private static final String SAVE_STATE_FORMAT_PRIVATE_DISK_DESC =
+            "StorageResetActivity.formatPrivateDiskDesc";
 
     private class SizeStringGetter extends StringGetter {
         private long mSize = INVALID_SIZE;
@@ -139,7 +150,7 @@
                 return;
             }
 
-            getFragmentManager().popBackStack(PROGRESS_DIALOG_BACKSTACK_TAG,
+            getFragmentManager().popBackStack(MOVE_PROGRESS_DIALOG_BACKSTACK_TAG,
                     FragmentManager.POP_BACK_STACK_INCLUSIVE);
 
             // TODO: refresh ui
@@ -154,6 +165,14 @@
         }
     };
 
+    // Non-null means we're in the process of formatting this volume
+    private String mFormatAsPrivateDiskId;
+    private String mFormatAsPrivateVolumeDesc;
+
+    private static final int LOADER_FORMAT_AS_PRIVATE = 0;
+
+    private final Handler mHandler = new Handler();
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -166,12 +185,23 @@
 
         mStorageManager = getSystemService(StorageManager.class);
         mStorageHeadersGetter.refreshView();
+
+        if (savedInstanceState != null) {
+            mFormatAsPrivateDiskId =
+                    savedInstanceState.getString(SAVE_STATE_FORMAT_PRIVATE_DISK_ID);
+            mFormatAsPrivateVolumeDesc =
+                    savedInstanceState.getString(SAVE_STATE_FORMAT_PRIVATE_DISK_DESC);
+
+            kickFormatAsPrivateLoader();
+        }
     }
 
     @Override
     protected void onSaveInstanceState(@NonNull Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putInt(SAVE_STATE_MOVE_ID, mAppMoveId);
+        outState.putString(SAVE_STATE_FORMAT_PRIVATE_DISK_ID, mFormatAsPrivateDiskId);
+        outState.putString(SAVE_STATE_FORMAT_PRIVATE_DISK_DESC, mFormatAsPrivateVolumeDesc);
     }
 
     @Override
@@ -489,7 +519,7 @@
                                 action.getData().getString(VolumeInfo.EXTRA_VOLUME_ID)));
                 getFragmentManager().beginTransaction()
                         .replace(android.R.id.content, f)
-                        .addToBackStack(null)
+                        .addToBackStack(FORMAT_DIALOG_BACKSTACK_TAG)
                         .commit();
                 break;
             }
@@ -501,7 +531,7 @@
                                 action.getData().getString(VolumeInfo.EXTRA_VOLUME_ID)));
                 getFragmentManager().beginTransaction()
                         .replace(android.R.id.content, f)
-                        .addToBackStack(null)
+                        .addToBackStack(FORMAT_DIALOG_BACKSTACK_TAG)
                         .commit();
             }
                 break;
@@ -632,40 +662,137 @@
         }
     }
 
-    public static class FormatAsPrivateTask extends AsyncTask<Void, Void, Exception> {
-        private final Context mContext;
+    private static class FormatAsPrivateTaskLoader
+            extends SettingsAsyncTaskLoader<Map<String, Object>> {
+
+        public static final String RESULT_EXCEPTION = "exception";
+        public static final String RESULT_INTERNAL_BENCH = "internalBench";
+        public static final String RESULT_PRIVATE_BENCH = "privateBench";
+
         private final StorageManager mStorageManager;
         private final String mDiskId;
+
+        public FormatAsPrivateTaskLoader(Context context, String diskId) {
+            super(context);
+            mStorageManager = getContext().getSystemService(StorageManager.class);
+            mDiskId = diskId;
+        }
+
+        @Override
+        protected void onDiscardResult(Map<String, Object> result) {}
+
+        @Override
+        public Map<String, Object> loadInBackground() {
+            final Map<String, Object> result = new ArrayMap<>(3);
+            try {
+                mStorageManager.partitionPrivate(mDiskId);
+                final Long internalBench = mStorageManager.benchmark(null);
+                result.put(RESULT_INTERNAL_BENCH, internalBench);
+
+                final VolumeInfo privateVol = findVolume();
+                if (privateVol != null) {
+                    final Long externalBench = mStorageManager.benchmark(privateVol.getId());
+                    result.put(RESULT_PRIVATE_BENCH, externalBench);
+                }
+            } catch (Exception e) {
+                result.put(RESULT_EXCEPTION, e);
+            }
+            return result;
+        }
+
+        private VolumeInfo findVolume() {
+            final List<VolumeInfo> vols = mStorageManager.getVolumes();
+            for (final VolumeInfo vol : vols) {
+                if (TextUtils.equals(mDiskId, vol.getDiskId())
+                        && (vol.getType() == VolumeInfo.TYPE_PRIVATE)) {
+                    return vol;
+                }
+            }
+            return null;
+        }
+    }
+
+    private class FormatAsPrivateLoaderCallback
+            implements LoaderManager.LoaderCallbacks<Map<String, Object>> {
+
+        private final String mDiskId;
         private final String mDescription;
 
-        public FormatAsPrivateTask(Context context, VolumeInfo volume) {
-            mContext = context.getApplicationContext();
-            mStorageManager = mContext.getSystemService(StorageManager.class);
-            mDiskId = volume.getDiskId();
-            mDescription = mStorageManager.getBestVolumeDescription(volume);
+        public FormatAsPrivateLoaderCallback(String diskId, String description) {
+            mDiskId = diskId;
+            mDescription = description;
         }
 
         @Override
-        protected Exception doInBackground(Void... params) {
-            try {
-                mStorageManager.partitionPrivate(mDiskId);
-                return null;
-            } catch (Exception e) {
-                return e;
+        public Loader<Map<String, Object>> onCreateLoader(int id, Bundle args) {
+            return new FormatAsPrivateTaskLoader(StorageResetActivity.this, mDiskId);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Map<String, Object>> loader, Map<String, Object> data) {
+            if (data == null) {
+                // No results yet, wait for something interesting to come in.
+                return;
             }
-        }
 
-        @Override
-        protected void onPostExecute(Exception e) {
+            final Exception e = (Exception) data.get(FormatAsPrivateTaskLoader.RESULT_EXCEPTION);
             if (e == null) {
-                Toast.makeText(mContext, mContext.getString(R.string.storage_format_success,
+                Toast.makeText(StorageResetActivity.this, getString(R.string.storage_format_success,
                         mDescription), Toast.LENGTH_SHORT).show();
+
+                final Long internalBench =
+                        (Long) data.get(FormatAsPrivateTaskLoader.RESULT_INTERNAL_BENCH);
+                final Long privateBench =
+                        (Long) data.get(FormatAsPrivateTaskLoader.RESULT_PRIVATE_BENCH);
+
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (isResumed() && TextUtils.equals(mDiskId, mFormatAsPrivateDiskId)) {
+                            final boolean popped = getFragmentManager().popBackStackImmediate(
+                                    FORMAT_DIALOG_BACKSTACK_TAG,
+                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
+                            if (internalBench != null && privateBench != null) {
+                                final float frac = (float) privateBench / (float) internalBench;
+                                Log.d(TAG, "New volume is " + frac + "x the speed of internal");
+
+                                // TODO: better threshold
+                                if (popped && privateBench > 2000000000) {
+                                    getFragmentManager().beginTransaction()
+                                            .addToBackStack(null)
+                                            .replace(android.R.id.content,
+                                                    SlowDriveStepFragment.newInstance())
+                                            .commit();
+                                }
+
+                                mFormatAsPrivateDiskId = null;
+                                mFormatAsPrivateVolumeDesc = null;
+                            }
+                        }
+                    }
+                });
+
             } else {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (isResumed() && TextUtils.equals(mDiskId, mFormatAsPrivateDiskId)) {
+                            getFragmentManager().popBackStack(FORMAT_DIALOG_BACKSTACK_TAG,
+                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
+                            mFormatAsPrivateDiskId = null;
+                            mFormatAsPrivateVolumeDesc = null;
+                        }
+                    }
+                });
+
                 Log.e(TAG, "Failed to format " + mDiskId, e);
-                Toast.makeText(mContext, mContext.getString(R.string.storage_format_failure,
+                Toast.makeText(StorageResetActivity.this, getString(R.string.storage_format_failure,
                         mDescription), Toast.LENGTH_SHORT).show();
             }
         }
+
+        @Override
+        public void onLoaderReset(Loader<Map<String, Object>> loader) {}
     }
 
     public static class FormatAsPublicTask extends AsyncTask<Void, Void, Exception> {
@@ -719,9 +846,38 @@
                 .newInstance(mPackageManager.getApplicationLabel(applicationInfo));
 
         getFragmentManager().beginTransaction()
-                .addToBackStack(PROGRESS_DIALOG_BACKSTACK_TAG)
+                .addToBackStack(MOVE_PROGRESS_DIALOG_BACKSTACK_TAG)
                 .replace(android.R.id.content, fragment)
                 .commit();
 
     }
+
+    @Override
+    public void onRequestFormatAsPrivate(VolumeInfo volumeInfo) {
+        final FormattingProgressFragment fragment = FormattingProgressFragment.newInstance();
+        getFragmentManager().popBackStack(FORMAT_DIALOG_BACKSTACK_TAG,
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+        getFragmentManager().beginTransaction()
+                .addToBackStack(FORMAT_DIALOG_BACKSTACK_TAG)
+                .replace(android.R.id.content, fragment)
+                .commit();
+
+        mFormatAsPrivateDiskId = volumeInfo.getDiskId();
+        mFormatAsPrivateVolumeDesc = mStorageManager.getBestVolumeDescription(volumeInfo);
+        kickFormatAsPrivateLoader();
+    }
+
+    private void kickFormatAsPrivateLoader() {
+        if (!TextUtils.isEmpty(mFormatAsPrivateDiskId)) {
+            getLoaderManager().initLoader(LOADER_FORMAT_AS_PRIVATE, null,
+                    new FormatAsPrivateLoaderCallback(mFormatAsPrivateDiskId,
+                            mFormatAsPrivateVolumeDesc));
+        }
+    }
+
+    @Override
+    public void onCancelFormatDialog() {
+        getFragmentManager().popBackStack(FORMAT_DIALOG_BACKSTACK_TAG,
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+    }
 }
diff --git a/Settings/src/com/android/tv/settings/device/storage/FormatAsPrivateStepFragment.java b/Settings/src/com/android/tv/settings/device/storage/FormatAsPrivateStepFragment.java
index 51d40e7..0a3aca9 100644
--- a/Settings/src/com/android/tv/settings/device/storage/FormatAsPrivateStepFragment.java
+++ b/Settings/src/com/android/tv/settings/device/storage/FormatAsPrivateStepFragment.java
@@ -25,7 +25,6 @@
 import android.support.v17.leanback.widget.GuidedAction;
 
 import com.android.tv.settings.R;
-import com.android.tv.settings.device.StorageResetActivity;
 
 import java.util.List;
 
@@ -37,6 +36,11 @@
 
     private StorageManager mStorageManager;
 
+    public interface Callback {
+        void onRequestFormatAsPrivate(VolumeInfo volumeInfo);
+        void onCancelFormatDialog();
+    }
+
     public static FormatAsPrivateStepFragment newInstance(VolumeInfo volumeInfo) {
         final FormatAsPrivateStepFragment fragment = new FormatAsPrivateStepFragment();
         final Bundle b = new Bundle(1);
@@ -56,7 +60,7 @@
     public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
         return new GuidanceStylist.Guidance(
                 getString(R.string.storage_wizard_format_as_private_title),
-                getString(R.string.storage_wizard_format__as_private_description), "",
+                getString(R.string.storage_wizard_format_as_private_description), "",
                 getActivity().getDrawable(R.drawable.ic_settings_storage));
     }
 
@@ -81,12 +85,13 @@
         final long id = action.getId();
 
         if (id == ACTION_ID_CANCEL) {
-            getFragmentManager().popBackStack();
+            final Callback callback = (Callback) getActivity();
+            callback.onCancelFormatDialog();
         } else if (id == ACTION_ID_FORMAT) {
             final VolumeInfo volumeInfo = mStorageManager.findVolumeById(
                     getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID));
-            new StorageResetActivity.FormatAsPrivateTask(getActivity(), volumeInfo).execute();
-            getFragmentManager().popBackStack();
+            final Callback callback = (Callback) getActivity();
+            callback.onRequestFormatAsPrivate(volumeInfo);
         } else if (id == ACTION_ID_LEARN_MORE) {
             // todo
         }
diff --git a/Settings/src/com/android/tv/settings/device/storage/FormattingProgressFragment.java b/Settings/src/com/android/tv/settings/device/storage/FormattingProgressFragment.java
new file mode 100644
index 0000000..6bb13da
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/storage/FormattingProgressFragment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 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.tv.settings.device.storage;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.view.View;
+
+import com.android.tv.settings.R;
+import com.android.tv.settings.dialog.ProgressDialogFragment;
+
+public class FormattingProgressFragment extends ProgressDialogFragment {
+
+    public static FormattingProgressFragment newInstance() {
+        return new FormattingProgressFragment();
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        setTitle(getActivity().getString(R.string.storage_wizard_format_progress_title));
+        setSummary(getActivity().getString(R.string.storage_wizard_format_progress_description));
+    }
+}
diff --git a/Settings/src/com/android/tv/settings/device/storage/SlowDriveStepFragment.java b/Settings/src/com/android/tv/settings/device/storage/SlowDriveStepFragment.java
new file mode 100644
index 0000000..0e1c6f0
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/storage/SlowDriveStepFragment.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 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.tv.settings.device.storage;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.settings.R;
+
+import java.util.List;
+
+public class SlowDriveStepFragment extends GuidedStepFragment {
+
+    public static SlowDriveStepFragment newInstance() {
+        return new SlowDriveStepFragment();
+    }
+
+    @Override
+    public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+        return new GuidanceStylist.Guidance(
+                getString(R.string.storage_wizard_format_slow_title),
+                getString(R.string.storage_wizard_format_slow_summary),
+                null,
+                getActivity().getDrawable(R.drawable.ic_settings_storage));
+    }
+
+    @Override
+    public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+        actions.add(new GuidedAction.Builder()
+                .title(getString(android.R.string.ok))
+                .build());
+    }
+
+    @Override
+    public void onGuidedActionClicked(GuidedAction action) {
+        getActivity().getFragmentManager().popBackStack();
+    }
+}
diff --git a/Settings/src/com/android/tv/settings/util/SettingsAsyncTaskLoader.java b/Settings/src/com/android/tv/settings/util/SettingsAsyncTaskLoader.java
new file mode 100644
index 0000000..bcfda3c
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/util/SettingsAsyncTaskLoader.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2015 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.tv.settings.util;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+
+/**
+ * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
+ *
+ * Subclasses need to implement {@link SettingsAsyncTaskLoader#loadInBackground()} to perform the
+ * actual background task, and {@link SettingsAsyncTaskLoader#onDiscardResult(T)} to clean up
+ * previously loaded results.
+ */
+
+public abstract class SettingsAsyncTaskLoader<T> extends AsyncTaskLoader<T> {
+    private T mResult;
+
+    public SettingsAsyncTaskLoader(final Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mResult != null) {
+            deliverResult(mResult);
+        }
+
+        if (takeContentChanged() || mResult == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    public void deliverResult(final T data) {
+        if (isReset()) {
+            if (data != null) {
+                onDiscardResult(data);
+            }
+            return;
+        }
+
+        final T oldResult = mResult;
+        mResult = data;
+
+        if (isStarted()) {
+            super.deliverResult(data);
+        }
+
+        if (oldResult != null && oldResult != mResult) {
+            onDiscardResult(oldResult);
+        }
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        onStopLoading();
+
+        if (mResult != null) {
+            onDiscardResult(mResult);
+        }
+        mResult = null;
+    }
+
+    @Override
+    public void onCanceled(final T data) {
+        super.onCanceled(data);
+
+        if (data != null) {
+            onDiscardResult(data);
+        }
+    }
+
+    /**
+     * Called when discarding the load results so subclasses can take care of clean-up or
+     * recycling tasks. This is not called if the same result (by way of pointer equality) is
+     * returned again by a subsequent call to loadInBackground, or if result is null.
+     *
+     * Note that this may be called concurrently with loadInBackground(), and in some circumstances
+     * may be called more than once for a given object.
+     *
+     * @param result The value returned from {@link SettingsAsyncTaskLoader#loadInBackground()} which
+     *               is to be discarded.
+     */
+    protected abstract void onDiscardResult(final T result);
+}