From 2ef20fb36dc99afbba76858c3a75d4aa9fb2fe34 Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Wed, 11 May 2016 12:54:13 -0700 Subject: [PATCH] Add downloads to the deletion helper. This adds a deletion service to delete files in the Downloads folder. All of the files there are collected and offered to the user to delete. Bug: 28621781 Change-Id: I94431f9abc3a0afa2d07dbab763312c09e830aef --- res/values/strings.xml | 9 ++ res/xml/deletion_helper_list.xml | 7 +- .../android/settings/DeletionPreference.java | 117 ++++++++++++++++++ .../settings/PhotosDeletionPreference.java | 93 ++------------ .../DeletionHelperFragment.java | 28 ++++- .../DownloadsDeletionPreference.java | 60 +++++++++ .../deletionhelper/DownloadsDeletionType.java | 109 ++++++++++++++++ .../deletionhelper/FetchDownloadsLoader.java | 96 ++++++++++++++ .../android/settings/utils/AsyncLoader.java | 109 ++++++++++++++++ .../FetchDownloadsLoaderTest.java | 90 ++++++++++++++ 10 files changed, 630 insertions(+), 88 deletions(-) create mode 100644 src/com/android/settings/DeletionPreference.java create mode 100644 src/com/android/settings/deletionhelper/DownloadsDeletionPreference.java create mode 100644 src/com/android/settings/deletionhelper/DownloadsDeletionType.java create mode 100644 src/com/android/settings/deletionhelper/FetchDownloadsLoader.java create mode 100644 src/com/android/settings/utils/AsyncLoader.java create mode 100644 tests/unit/src/com/android/settings/deletionhelper/FetchDownloadsLoaderTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 00c11301c1f..f533d89565a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7589,4 +7589,13 @@ %1$s, older than %2$d days + + Downloads (%1$d) + + + %1$s, last modified %2$s + + + %1$s + diff --git a/res/xml/deletion_helper_list.xml b/res/xml/deletion_helper_list.xml index 5affd601bf7..78d3b144153 100644 --- a/res/xml/deletion_helper_list.xml +++ b/res/xml/deletion_helper_list.xml @@ -20,8 +20,11 @@ + + + android:key="apps_group" + android:title="@string/deletion_helper_apps_title" /> diff --git a/src/com/android/settings/DeletionPreference.java b/src/com/android/settings/DeletionPreference.java new file mode 100644 index 00000000000..93d76e6d9ad --- /dev/null +++ b/src/com/android/settings/DeletionPreference.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 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; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.support.v7.preference.CheckBoxPreference; +import android.support.v7.preference.PreferenceViewHolder; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.text.format.Formatter; +import android.widget.TextView; +import com.android.settings.deletionhelper.DeletionType; + +/** + * Preference to handle the deletion of various data types in the Deletion Helper. + */ +public abstract class DeletionPreference extends CheckBoxPreference implements + DeletionType.FreeableChangedListener, OnPreferenceChangeListener { + private DeletionType.FreeableChangedListener mListener; + private boolean mChecked; + private long mFreeableBytes; + private int mFreeableItems; + private DeletionType mDeletionService; + + public DeletionPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setOnPreferenceChangeListener(this); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + final TextView titleView = (TextView) holder.findViewById(android.R.id.title); + if (titleView != null) { + titleView.setTextColor(getTintColor(getContext())); + } + } + + /** + * Returns the number of bytes which can be cleared by the deletion service. + * @return The number of bytes. + */ + public long getFreeableBytes() { + return mChecked ? mFreeableBytes : 0; + } + + /** + * Register a listener to be called back on when the freeable bytes have changed. + * @param listener The callback listener. + */ + public void registerFreeableChangedListener(DeletionType.FreeableChangedListener listener) { + mListener = listener; + } + + /** + * Registers a deletion service to update the preference's information. + * @param deletionService A photo/video deletion service. + */ + public void registerDeletionService(DeletionType deletionService) { + mDeletionService = deletionService; + if (mDeletionService != null) { + mDeletionService.registerFreeableChangedListener(this); + } + } + + /** + * Returns the deletion service powering the preference. + * @return The deletion service. + */ + public DeletionType getDeletionService() { + return mDeletionService; + } + + @Override + public void onFreeableChanged(int numItems, long freeableBytes) { + mFreeableItems = numItems; + mFreeableBytes = freeableBytes; + maybeUpdateListener(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + mChecked = (boolean) newValue; + maybeUpdateListener(); + return true; + } + + private int getTintColor(Context context) { + TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true); + return context.getColor(value.resourceId); + } + + private void maybeUpdateListener() { + if (mListener != null) { + mListener.onFreeableChanged(mFreeableItems, getFreeableBytes()); + } + } +} diff --git a/src/com/android/settings/PhotosDeletionPreference.java b/src/com/android/settings/PhotosDeletionPreference.java index 63327918598..8c98370c7d0 100644 --- a/src/com/android/settings/PhotosDeletionPreference.java +++ b/src/com/android/settings/PhotosDeletionPreference.java @@ -19,107 +19,36 @@ package com.android.settings; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.support.v7.preference.Preference; -import android.support.v7.preference.Preference.OnPreferenceChangeListener; -import android.support.v7.preference.CheckBoxPreference; -import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; -import android.util.TypedValue; import android.text.format.Formatter; -import android.widget.TextView; -import com.android.settings.deletionhelper.DeletionType; /** * Preference to handle the deletion of photos and videos in the Deletion Helper. */ -public class PhotosDeletionPreference extends CheckBoxPreference implements - DeletionType.FreeableChangedListener, OnPreferenceChangeListener { +public class PhotosDeletionPreference extends DeletionPreference { // TODO(b/28560570): Remove this dummy value. private static final int FAKE_DAYS_TO_KEEP = 30; - private DeletionType.FreeableChangedListener mListener; - private boolean mChecked; - private long mFreeableBytes; - private int mFreeableItems; - private DeletionType mDeletionService; public PhotosDeletionPreference(Context context, AttributeSet attrs) { super(context, attrs); setIcon(getIcon(context)); - updatePreferenceText(); - setOnPreferenceChangeListener(this); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - final TextView titleView = (TextView) holder.findViewById(android.R.id.title); - if (titleView != null) { - titleView.setTextColor(getTintColor(getContext())); - } - } - - /** - * Get the tint color for the preference's icon and text. - * @param context UI context to get the theme. - * @return The tint color. - */ - public int getTintColor(Context context) { - TypedValue value = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true); - return context.getColor(value.resourceId); + updatePreferenceText(0, 0); } /** * Updates the title and summary of the preference with fresh information. */ - public void updatePreferenceText() { + public void updatePreferenceText(int items, long bytes) { Context context = getContext(); - setTitle(context.getString(R.string.deletion_helper_photos_title, - mFreeableItems)); + setTitle(context.getString(R.string.deletion_helper_photos_title, items)); setSummary(context.getString(R.string.deletion_helper_photos_summary, - Formatter.formatFileSize(context, mFreeableBytes), FAKE_DAYS_TO_KEEP)); - } - - /** - * Returns the number of bytes which can be cleared by the deletion service. - * @return The number of bytes. - */ - public long getFreeableBytes() { - return mChecked ? mFreeableBytes : 0; - } - - /** - * Register a listener to be called back on when the freeable bytes have changed. - * @param listener The callback listener. - */ - public void registerFreeableChangedListener(DeletionType.FreeableChangedListener listener) { - mListener = listener; - } - - /** - * Registers a deletion service to update the preference's information. - * @param deletionService A photo/video deletion service. - */ - public void registerDeletionService(DeletionType deletionService) { - mDeletionService = deletionService; - if (mDeletionService != null) { - mDeletionService.registerFreeableChangedListener(this); - } + Formatter.formatFileSize(context, bytes), FAKE_DAYS_TO_KEEP)); } @Override - public void onFreeableChanged(int numItems, long freeableBytes) { - mFreeableItems = numItems; - mFreeableBytes = freeableBytes; - updatePreferenceText(); - maybeUpdateListener(); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - mChecked = (boolean) newValue; - maybeUpdateListener(); - return true; + public void onFreeableChanged(int items, long bytes) { + super.onFreeableChanged(items, bytes); + updatePreferenceText(items, bytes); } private Drawable getIcon(Context context) { @@ -134,10 +63,4 @@ public class PhotosDeletionPreference extends CheckBoxPreference implements } return iconDrawable; } - - private void maybeUpdateListener() { - if (mListener != null) { - mListener.onFreeableChanged(mFreeableItems, getFreeableBytes()); - } - } } diff --git a/src/com/android/settings/deletionhelper/DeletionHelperFragment.java b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java index 291e5250b61..b4c03e728bf 100644 --- a/src/com/android/settings/deletionhelper/DeletionHelperFragment.java +++ b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java @@ -25,6 +25,7 @@ import android.util.ArraySet; import android.util.Log; import android.view.View; import android.widget.Button; +import com.android.settings.deletionhelper.DownloadsDeletionPreference; import com.android.settings.PhotosDeletionPreference; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.R; @@ -54,10 +55,14 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement private static final String KEY_APPS_GROUP = "apps_group"; private static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos"; + private static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads"; + + private static final int DOWNLOADS_LOADER_ID = 1; private Button mCancel, mFree; private PreferenceGroup mApps; private PhotosDeletionPreference mPhotoPreference; + private DownloadsDeletionPreference mDownloadsPreference; private ApplicationsState mState; private Session mSession; @@ -67,6 +72,7 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement private boolean mHasReceivedAppEntries, mHasReceivedBridgeCallback, mFinishedLoading; private DeletionHelperFeatureProvider mProvider; private DeletionType mPhotoVideoDeletion; + private DownloadsDeletionType mDownloadsDeletion; @Override public void onCreate(Bundle savedInstanceState) { @@ -80,11 +86,14 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement addPreferencesFromResource(R.xml.deletion_helper_list); mApps = (PreferenceGroup) findPreference(KEY_APPS_GROUP); mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE); + mDownloadsPreference = + (DownloadsDeletionPreference) findPreference(KEY_DOWNLOADS_PREFERENCE); mProvider = FeatureFactory.getFactory(app).getDeletionHelperFeatureProvider(); if (mProvider != null) { mPhotoVideoDeletion = mProvider.createPhotoVideoDeletionType(); } + mDownloadsDeletion = new DownloadsDeletionType(getActivity()); if (savedInstanceState != null) { mHasReceivedAppEntries = @@ -117,6 +126,9 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement if (mPhotoPreference != null && mPhotoPreference.isChecked()) { mPhotoVideoDeletion.clearFreeableData(); } + if (mDownloadsPreference != null && mDownloadsPreference.isChecked()) { + mDownloadsDeletion.clearFreeableData(); + } ArraySet apps = new ArraySet<>(); for (AppEntry entry : mAppEntries) { @@ -153,6 +165,9 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement mPhotoPreference.registerFreeableChangedListener(this); mPhotoPreference.registerDeletionService(mPhotoVideoDeletion); } + + mDownloadsPreference.registerFreeableChangedListener(this); + mDownloadsPreference.registerDeletionService(mDownloadsDeletion); } @Override @@ -172,6 +187,10 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement if (mPhotoVideoDeletion != null) { mPhotoVideoDeletion.onResume(); } + if (mDownloadsDeletion != null) { + mDownloadsDeletion.onResume(); + getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion); + } } @@ -193,6 +212,9 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement if (mPhotoVideoDeletion != null) { mPhotoVideoDeletion.onPause(); } + if (mDownloadsDeletion != null) { + mDownloadsDeletion.onPause(); + } } private void rebuild() { @@ -211,7 +233,8 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement for (int i = 0; i < entryCount; i++) { AppEntry entry = apps.get(i); final String packageName = entry.label; - AppDeletionPreference preference = (AppDeletionPreference) getCachedPreference(entry.label); + AppDeletionPreference preference = + (AppDeletionPreference) getCachedPreference(entry.label); if (preference == null) { preference = new AppDeletionPreference(getActivity(), entry, mState); @@ -322,6 +345,9 @@ public class DeletionHelperFragment extends SettingsPreferenceFragment implement if (mPhotoPreference != null) { freeableSpace += mPhotoPreference.getFreeableBytes(); } + if (mDownloadsPreference != null) { + freeableSpace += mDownloadsPreference.getFreeableBytes(); + } return freeableSpace; } } \ No newline at end of file diff --git a/src/com/android/settings/deletionhelper/DownloadsDeletionPreference.java b/src/com/android/settings/deletionhelper/DownloadsDeletionPreference.java new file mode 100644 index 00000000000..7cddf321a99 --- /dev/null +++ b/src/com/android/settings/deletionhelper/DownloadsDeletionPreference.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 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.deletionhelper; + +import android.content.Context; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.text.format.Formatter; +import com.android.settings.DeletionPreference; +import com.android.settings.R; + +/** + * Preference to handle the deletion of photos and videos in the Deletion Helper. + */ +public class DownloadsDeletionPreference extends DeletionPreference { + public DownloadsDeletionPreference(Context context, AttributeSet attrs) { + super(context, attrs); + updatePreferenceText(0, 0, Long.MAX_VALUE); + } + + @Override + public void onFreeableChanged(int numItems, long freeableBytes) { + super.onFreeableChanged(numItems, freeableBytes); + DownloadsDeletionType deletionService = (DownloadsDeletionType) getDeletionService(); + updatePreferenceText(numItems, freeableBytes, deletionService.getMostRecentLastModified()); + } + + private void updatePreferenceText(int items, long bytes, long mostRecent) { + Context context = getContext(); + setTitle(context.getString(R.string.deletion_helper_downloads_title, + items)); + // If there are no files to clear, show the empty text instead. + if (mostRecent < Long.MAX_VALUE) { + setSummary(context.getString(R.string.deletion_helper_downloads_summary, + Formatter.formatFileSize(context, bytes), + DateUtils.getRelativeTimeSpanString(mostRecent, + System.currentTimeMillis(), + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE))); + } else { + setSummary(context.getString(R.string.deletion_helper_downloads_summary_empty, + Formatter.formatFileSize(context, bytes))); + } + } + +} diff --git a/src/com/android/settings/deletionhelper/DownloadsDeletionType.java b/src/com/android/settings/deletionhelper/DownloadsDeletionType.java new file mode 100644 index 00000000000..81293d67a51 --- /dev/null +++ b/src/com/android/settings/deletionhelper/DownloadsDeletionType.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 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.deletionhelper; + +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; +import android.content.Loader; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Environment; +import com.android.settings.deletionhelper.FetchDownloadsLoader.DownloadsResult; + +import java.io.File; +import java.util.ArrayList; + +/** + * The DownloadsDeletionType provides stale download file information to the + * {@link DownloadsDeletionPreference}. + */ +public class DownloadsDeletionType implements DeletionType, LoaderCallbacks { + private int mItems; + private long mBytes; + private long mMostRecent; + private FreeableChangedListener mListener; + private FetchDownloadsLoader mTask; + private ArrayList mFiles; + private Context mContext; + + public DownloadsDeletionType(Context context) { + mContext = context; + } + + @Override + public void registerFreeableChangedListener(FreeableChangedListener listener) { + mListener = listener; + if (mFiles != null) { + maybeUpdateListener(); + } + } + + @Override + public void onResume() { + } + + @Override + public void onPause() { + } + + @Override + public void clearFreeableData() { + if (mFiles != null) { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + for (File file : mFiles) { + file.delete(); + } + } + }); + } + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new FetchDownloadsLoader(mContext, + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)); + } + + @Override + public void onLoadFinished(Loader loader, DownloadsResult data) { + mMostRecent = data.youngestLastModified; + mFiles = data.files; + mBytes = data.totalSize; + mItems = mFiles.size(); + maybeUpdateListener(); + } + + @Override + public void onLoaderReset(Loader loader) { + } + + /** + * Returns the most recent last modified time for any clearable file. + * @return The last modified time. + */ + public long getMostRecentLastModified() { + return mMostRecent; + } + + private void maybeUpdateListener() { + if (mListener != null) { + mListener.onFreeableChanged(mItems, mBytes); + } + } +} diff --git a/src/com/android/settings/deletionhelper/FetchDownloadsLoader.java b/src/com/android/settings/deletionhelper/FetchDownloadsLoader.java new file mode 100644 index 00000000000..86352c918a0 --- /dev/null +++ b/src/com/android/settings/deletionhelper/FetchDownloadsLoader.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 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.deletionhelper; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import com.android.settings.utils.AsyncLoader; + +import java.io.File; +import java.util.ArrayList; + +/** + * FetchDownloadsLoader is an asynchronous task which returns files in the Downloads + * directory which have not been modified in longer than 90 days. + */ +public class FetchDownloadsLoader extends + AsyncLoader { + private File mDirectory; + + /** + * Sets up a FetchDownloadsLoader in any directory. + * @param directory The directory to look into. + */ + public FetchDownloadsLoader(Context context, File directory) { + super(context); + mDirectory = directory; + } + + @Override + protected void onDiscardResult(DownloadsResult result) {} + + @Override + public DownloadsResult loadInBackground() { + return collectFiles(mDirectory); + } + + @VisibleForTesting + static DownloadsResult collectFiles(File dir) { + return collectFiles(dir, new DownloadsResult()); + } + + private static DownloadsResult collectFiles(File dir, DownloadsResult result) { + File downloadFiles[] = dir.listFiles(); + if (downloadFiles == null) { + } + if (downloadFiles != null && downloadFiles.length > 0) { + for (File currentFile : downloadFiles) { + if (currentFile.isDirectory()) { + collectFiles(currentFile, result); + } else { + if (currentFile.lastModified() < result.youngestLastModified) { + result.youngestLastModified = currentFile.lastModified(); + } + result.files.add(currentFile); + result.totalSize += currentFile.length(); + } + } + } + + return result; + } + + /** + * The DownloadsResult is the result of a {@link FetchDownloadsLoader} with the files + * and the amount of space they use. + */ + public static class DownloadsResult { + public long totalSize; + public long youngestLastModified; + public ArrayList files; + + public DownloadsResult() { + this(0, Long.MAX_VALUE, new ArrayList()); + } + + public DownloadsResult(long totalSize, long youngestLastModified, ArrayList files) { + this.totalSize = totalSize; + this.youngestLastModified = youngestLastModified; + this.files = files; + } + } +} \ No newline at end of file diff --git a/src/com/android/settings/utils/AsyncLoader.java b/src/com/android/settings/utils/AsyncLoader.java new file mode 100644 index 00000000000..76c99fa4793 --- /dev/null +++ b/src/com/android/settings/utils/AsyncLoader.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 Google Inc. + * Licensed to 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.utils; + +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 AsyncLoader#loadInBackground()} to perform the actual + * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded + * results. + * + * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo. + */ +public abstract class AsyncLoader extends AsyncTaskLoader { + private T mResult; + + public AsyncLoader(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 AsyncLoader#loadInBackground()} which + * is to be discarded. + */ + protected abstract void onDiscardResult(final T result); +} diff --git a/tests/unit/src/com/android/settings/deletionhelper/FetchDownloadsLoaderTest.java b/tests/unit/src/com/android/settings/deletionhelper/FetchDownloadsLoaderTest.java new file mode 100644 index 00000000000..52312d1b763 --- /dev/null +++ b/tests/unit/src/com/android/settings/deletionhelper/FetchDownloadsLoaderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.deletionhelper; + +import com.android.settings.deletionhelper.FetchDownloadsLoader.DownloadsResult; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.rules.TemporaryFolder; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.FileWriter; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; + +@RunWith(JUnit4.class) +public class FetchDownloadsLoaderTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testEmptyDirectory() throws Exception { + DownloadsResult result = + FetchDownloadsLoader.collectFiles(temporaryFolder.getRoot()); + assertNotNull(result); + assertEquals(0, result.totalSize); + assertEquals(0, result.files.size()); + } + + @Test + public void testFilesInDirectory() throws Exception { + temporaryFolder.newFile(); + temporaryFolder.newFile(); + + DownloadsResult result = + FetchDownloadsLoader.collectFiles(temporaryFolder.getRoot()); + assertNotNull(result); + assertEquals(0, result.totalSize); + assertEquals(2, result.files.size()); + } + + @Test + public void testNestedDirectories() throws Exception { + File tempDir = temporaryFolder.newFolder(); + + File testFile = File.createTempFile("test", null, tempDir); + testFile.deleteOnExit(); + DownloadsResult result = + FetchDownloadsLoader.collectFiles(temporaryFolder.getRoot()); + assertNotNull(result); + assertEquals(0, result.totalSize); + assertEquals(1, result.files.size()); + } + + @Test + public void testSumFileSizes() throws Exception { + File first = temporaryFolder.newFile(); + FileWriter fileWriter = new FileWriter(first); + fileWriter.write("test"); + fileWriter.close(); + + File second = temporaryFolder.newFile(); + fileWriter = new FileWriter(second); + fileWriter.write("test2"); + fileWriter.close(); + + DownloadsResult result = + FetchDownloadsLoader.collectFiles(temporaryFolder.getRoot()); + assertNotNull(result); + assertEquals(9, result.totalSize); + assertEquals(2, result.files.size()); + } +}