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