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;
21 import android.app.usage.UsageStatsManager;
22 import android.content.Context;
23 import android.content.Loader;
24 import android.os.Bundle;
25 import android.os.UserHandle;
26 import android.os.storage.VolumeInfo;
27 import android.util.ArraySet;
28 import android.util.Log;
29 import com.android.internal.logging.MetricsLogger;
30 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
31 import com.android.settingslib.applications.StorageStatsSource;
32 import com.android.storagemanager.deletionhelper.AppsAsyncLoader.AppFilter;
33 import com.android.storagemanager.deletionhelper.AppsAsyncLoader.PackageInfo;
34 import java.util.HashSet;
35 import java.util.List;
36 
37 /**
38  * AppDeletionType provides a list of apps which have not been used for a while on the system. It
39  * also provides the functionality to clear out these apps.
40  */
41 public class AppDeletionType
42         implements LoaderManager.LoaderCallbacks<List<PackageInfo>>, DeletionType {
43     public static final String EXTRA_CHECKED_SET = "checkedSet";
44     private static final String TAG = "AppDeletionType";
45     private static final int LOADER_ID = 25;
46     public static final String THRESHOLD_TYPE_KEY = "threshold_type";
47     public static final int BUNDLE_CAPACITY = 1;
48 
49     private FreeableChangedListener mListener;
50     private AppListener mAppListener;
51     private HashSet<String> mCheckedApplications;
52     private Context mContext;
53     private int mThresholdType;
54     private List<PackageInfo> mApps;
55     private int mLoadingStatus;
56 
AppDeletionType( DeletionHelperSettings fragment, HashSet<String> checkedApplications, int thresholdType)57     public AppDeletionType(
58             DeletionHelperSettings fragment,
59             HashSet<String> checkedApplications,
60             int thresholdType) {
61         mLoadingStatus = LoadingStatus.LOADING;
62         mThresholdType = thresholdType;
63         mContext = fragment.getContext();
64         if (checkedApplications != null) {
65             mCheckedApplications = checkedApplications;
66         } else {
67             mCheckedApplications = new HashSet<>();
68         }
69         Bundle bundle = new Bundle(BUNDLE_CAPACITY);
70         bundle.putInt(THRESHOLD_TYPE_KEY, mThresholdType);
71         // NOTE: This is not responsive to package changes. Bug filed for seeing if feature is
72         // necessary b/35065979
73         fragment.getLoaderManager().initLoader(LOADER_ID, bundle, this);
74     }
75 
76     @Override
registerFreeableChangedListener(FreeableChangedListener listener)77     public void registerFreeableChangedListener(FreeableChangedListener listener) {
78         mListener = listener;
79     }
80 
81     @Override
onResume()82     public void onResume() {
83 
84     }
85 
86     @Override
onPause()87     public void onPause() {
88 
89     }
90 
91     @Override
onSaveInstanceStateBundle(Bundle savedInstanceState)92     public void onSaveInstanceStateBundle(Bundle savedInstanceState) {
93         savedInstanceState.putSerializable(EXTRA_CHECKED_SET, mCheckedApplications);
94     }
95 
96     @Override
clearFreeableData(Activity activity)97     public void clearFreeableData(Activity activity) {
98         if (mApps == null) {
99             return;
100         }
101 
102         ArraySet<String> apps = new ArraySet<>();
103         for (PackageInfo app : mApps) {
104             final String packageName = app.packageName;
105             if (mCheckedApplications.contains(packageName)) {
106                 apps.add(packageName);
107             }
108         }
109         // TODO: If needed, add an action on the callback.
110         PackageDeletionTask task = new PackageDeletionTask(activity.getPackageManager(), apps,
111                 new PackageDeletionTask.Callback() {
112                     @Override
113                     public void onSuccess() {
114                     }
115 
116                     @Override
117                     public void onError() {
118                         Log.e(TAG, "An error occurred while uninstalling packages.");
119                         MetricsLogger.action(activity,
120                                 MetricsEvent.ACTION_DELETION_HELPER_APPS_DELETION_FAIL);
121                     }
122                 });
123 
124         task.run();
125     }
126 
127     /**
128      * Registers a preference group view to notify when the app list changes.
129      */
registerView(AppDeletionPreferenceGroup preference)130     public void registerView(AppDeletionPreferenceGroup preference) {
131         mAppListener = preference;
132     }
133 
134     /**
135      * Set a package to be checked for deletion, if the apps are cleared.
136      * @param packageName The name of the package to potentially delete.
137      * @param isChecked Whether or not the package should be deleted.
138      */
setChecked(String packageName, boolean isChecked)139     public void setChecked(String packageName, boolean isChecked) {
140         if (isChecked) {
141             mCheckedApplications.add(packageName);
142         } else {
143             mCheckedApplications.remove(packageName);
144         }
145         maybeNotifyListener();
146     }
147 
148     /**
149      * Returns an amount of clearable app data.
150      * @param countUnchecked If unchecked applications should be counted for size purposes.
151      */
getTotalAppsFreeableSpace(boolean countUnchecked)152     public long getTotalAppsFreeableSpace(boolean countUnchecked) {
153         long freeableSpace = 0;
154         if (mApps != null) {
155             for (int i = 0, size = mApps.size(); i < size; i++) {
156                 final PackageInfo app = mApps.get(i);
157                 long appSize = app.size;
158                 final String packageName = app.packageName;
159                 // If the appSize is negative, it is either an unknown size or an error occurred.
160                 if ((countUnchecked || mCheckedApplications.contains(packageName)) && appSize > 0) {
161                     freeableSpace += appSize;
162                 }
163             }
164         }
165 
166         return freeableSpace;
167     }
168 
169     /**
170      * Returns if a given package is slated for deletion.
171      * @param packageName The name of the package to check.
172      */
isChecked(String packageName)173     public boolean isChecked(String packageName) {
174         return mCheckedApplications.contains(packageName);
175     }
176 
getFilter(int mThresholdType)177     private AppFilter getFilter(int mThresholdType) {
178         switch (mThresholdType) {
179             case AppsAsyncLoader.NO_THRESHOLD:
180                 return AppsAsyncLoader.FILTER_NO_THRESHOLD;
181             case AppsAsyncLoader.NORMAL_THRESHOLD:
182             default:
183                 return AppsAsyncLoader.FILTER_USAGE_STATS;
184         }
185     }
186 
maybeNotifyListener()187     private void maybeNotifyListener() {
188         if (mListener != null) {
189             mListener.onFreeableChanged(
190                     mApps.size(),
191                     getTotalAppsFreeableSpace(DeletionHelperSettings.COUNT_CHECKED_ONLY));
192         }
193     }
194 
getDeletionThreshold()195     public long getDeletionThreshold() {
196         switch (mThresholdType) {
197             case AppsAsyncLoader.NO_THRESHOLD:
198                 // The threshold is actually Long.MIN_VALUE but we don't want to display that to
199                 // the user.
200                 return 0;
201             case AppsAsyncLoader.NORMAL_THRESHOLD:
202             default:
203                 return AppsAsyncLoader.UNUSED_DAYS_DELETION_THRESHOLD;
204         }
205     }
206 
207     @Override
getLoadingStatus()208     public int getLoadingStatus() {
209         return mLoadingStatus;
210     }
211 
212     @Override
getContentCount()213     public int getContentCount() {
214         return mApps.size();
215     }
216 
217     @Override
setLoadingStatus(@oadingStatus int loadingStatus)218     public void setLoadingStatus(@LoadingStatus int loadingStatus) {
219         mLoadingStatus = loadingStatus;
220     }
221 
222     @Override
onCreateLoader(int id, Bundle args)223     public Loader<List<PackageInfo>> onCreateLoader(int id, Bundle args) {
224         return new AppsAsyncLoader.Builder(mContext)
225                 .setUid(UserHandle.myUserId())
226                 .setUuid(VolumeInfo.ID_PRIVATE_INTERNAL)
227                 .setStorageStatsSource(new StorageStatsSource(mContext))
228                 .setPackageManager(mContext.getPackageManager())
229                 .setUsageStatsManager(
230                         (UsageStatsManager) mContext.getSystemService(Context.USAGE_STATS_SERVICE))
231                 .setFilter(
232                         getFilter(
233                                 args.getInt(THRESHOLD_TYPE_KEY, AppsAsyncLoader.NORMAL_THRESHOLD)))
234                 .build();
235     }
236 
237     @Override
onLoadFinished(Loader<List<PackageInfo>> loader, List<PackageInfo> data)238     public void onLoadFinished(Loader<List<PackageInfo>> loader, List<PackageInfo> data) {
239         mApps = data;
240         updateLoadingStatus();
241         maybeNotifyListener();
242         mAppListener.onAppRebuild(mApps);
243     }
244 
245     @Override
onLoaderReset(Loader<List<PackageInfo>> loader)246     public void onLoaderReset(Loader<List<PackageInfo>> loader) {}
247 
248     /**
249      * An interface for listening for when the app list has been rebuilt.
250      */
251     public interface AppListener {
252         /**
253          * Callback to be called once the app list is rebuilt.
254          *
255          * @param apps A list of eligible, clearable AppEntries.
256          */
onAppRebuild(List<PackageInfo> apps)257         void onAppRebuild(List<PackageInfo> apps);
258     }
259 }
260