1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.storagemanager.deletionhelper;
18 
19 import android.app.Activity;
20 import android.app.LoaderManager.LoaderCallbacks;
21 import android.content.Context;
22 import android.content.Loader;
23 import android.graphics.Bitmap;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.os.Environment;
27 import androidx.annotation.Nullable;
28 import android.util.ArraySet;
29 
30 import com.android.internal.logging.MetricsLogger;
31 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
32 import com.android.storagemanager.deletionhelper.FetchDownloadsLoader.DownloadsResult;
33 
34 import java.io.File;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.Set;
38 
39 /**
40  * The DownloadsDeletionType provides stale download file information to the
41  * {@link DownloadsDeletionPreferenceGroup}.
42  */
43 public class DownloadsDeletionType implements DeletionType, LoaderCallbacks<DownloadsResult> {
44     public static final String EXTRA_UNCHECKED_DOWNLOADS = "uncheckedFiles";
45     private long mBytes;
46     private long mMostRecent;
47     private FreeableChangedListener mListener;
48     private Context mContext;
49     private ArraySet<File> mFiles;
50     private ArraySet<String> mUncheckedFiles;
51     private HashMap<File, Bitmap> mThumbnails;
52     private int mLoadingStatus;
53 
DownloadsDeletionType(Context context, String[] uncheckedFiles)54     public DownloadsDeletionType(Context context, String[] uncheckedFiles) {
55         mLoadingStatus = LoadingStatus.LOADING;
56         mContext = context;
57         mFiles = new ArraySet<>();
58         mUncheckedFiles = new ArraySet<>();
59         if (uncheckedFiles != null) {
60             Collections.addAll(mUncheckedFiles, uncheckedFiles);
61         }
62     }
63 
64     @Override
registerFreeableChangedListener(FreeableChangedListener listener)65     public void registerFreeableChangedListener(FreeableChangedListener listener) {
66         mListener = listener;
67         if (mFiles != null) {
68             maybeUpdateListener();
69         }
70     }
71 
72     @Override
onResume()73     public void onResume() {
74     }
75 
76     @Override
onPause()77     public void onPause() {
78     }
79 
80     @Override
onSaveInstanceStateBundle(Bundle savedInstanceState)81     public void onSaveInstanceStateBundle(Bundle savedInstanceState) {
82         savedInstanceState.putStringArray(EXTRA_UNCHECKED_DOWNLOADS,
83                 mUncheckedFiles.toArray(new String[mUncheckedFiles.size()]));
84     }
85 
86     @Override
clearFreeableData(Activity activity)87     public void clearFreeableData(Activity activity) {
88         if (mFiles != null) {
89             AsyncTask.execute(new Runnable() {
90                 @Override
91                 public void run() {
92                     boolean succeeded = true;
93                     for (File entry : mFiles) {
94                         if (isChecked(entry)) {
95                             succeeded = succeeded && entry.delete();
96                         }
97                     }
98 
99                     if (!succeeded) {
100                         MetricsLogger.action(activity,
101                                 MetricsEvent.ACTION_DELETION_HELPER_DOWNLOADS_DELETION_FAIL);
102                     }
103                 }
104             });
105         }
106     }
107 
108     @Override
getLoadingStatus()109     public int getLoadingStatus() {
110         return mLoadingStatus;
111     }
112 
113     @Override
getContentCount()114     public int getContentCount() {
115         return mFiles.size();
116     }
117 
118     @Override
setLoadingStatus(@oadingStatus int loadingStatus)119     public void setLoadingStatus(@LoadingStatus int loadingStatus) {
120         mLoadingStatus = loadingStatus;
121     }
122 
123     @Override
onCreateLoader(int id, Bundle args)124     public Loader<DownloadsResult> onCreateLoader(int id, Bundle args) {
125         return new FetchDownloadsLoader(mContext,
126                 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
127     }
128 
129     @Override
onLoadFinished(Loader<DownloadsResult> loader, DownloadsResult data)130     public void onLoadFinished(Loader<DownloadsResult> loader, DownloadsResult data) {
131         mMostRecent = data.youngestLastModified;
132         for (File file : data.files) {
133             mFiles.add(file);
134         }
135         mBytes = data.totalSize;
136         mThumbnails = data.thumbnails;
137         updateLoadingStatus();
138         maybeUpdateListener();
139     }
140 
141     @Override
onLoaderReset(Loader<DownloadsResult> loader)142     public void onLoaderReset(Loader<DownloadsResult> loader) {
143     }
144 
145     /**
146      * Returns the most recent last modified time for any clearable file.
147      * @return The last modified time.
148      */
getMostRecentLastModified()149     public long getMostRecentLastModified() {
150         return mMostRecent;
151     }
152 
153     /**
154      * Returns the files in the Downloads folder after the loader task finishes.
155      */
getFiles()156     public Set<File> getFiles() {
157         if (mFiles == null) {
158             return null;
159         }
160         return mFiles;
161     }
162 
163     /**
164      * Set if a file should be deleted when the service is asked to clear files.
165      */
setFileChecked(File file, boolean checked)166     public void setFileChecked(File file, boolean checked) {
167         if (checked) {
168             mUncheckedFiles.remove(file.getPath());
169         } else {
170             mUncheckedFiles.add(file.getPath());
171         }
172     }
173 
174     /** Returns the number of bytes that would be cleared if the deletion tasks runs. */
getFreeableBytes(boolean countUnchecked)175     public long getFreeableBytes(boolean countUnchecked) {
176         long freedBytes = 0;
177         for (File file : mFiles) {
178             if (isChecked(file) || countUnchecked) {
179                 freedBytes += file.length();
180             }
181         }
182         return freedBytes;
183     }
184 
185     /** Returns a thumbnail for a given file, if it exists. If it does not exist, returns null. */
getCachedThumbnail(File imageFile)186     public @Nullable Bitmap getCachedThumbnail(File imageFile) {
187         if (mThumbnails == null) {
188             return null;
189         }
190         return mThumbnails.get(imageFile);
191     }
192 
193     /**
194      * Return if a given file is checked for deletion.
195      *
196      * @param file The file to check.
197      */
isChecked(File file)198     public boolean isChecked(File file) {
199         return !mUncheckedFiles.contains(file.getPath());
200     }
201 
maybeUpdateListener()202     private void maybeUpdateListener() {
203         if (mListener != null) {
204             mListener.onFreeableChanged(mFiles.size(), mBytes);
205         }
206     }
207 }
208