1 /*
2  * Copyright (C) 2015 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.tv.settings.device.storage;
18 
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageManager;
21 import android.graphics.drawable.Drawable;
22 import android.os.AsyncTask;
23 import android.os.Bundle;
24 import android.os.storage.StorageManager;
25 import android.os.storage.VolumeInfo;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 
29 import androidx.annotation.NonNull;
30 import androidx.leanback.app.GuidedStepFragment;
31 import androidx.leanback.widget.GuidanceStylist;
32 import androidx.leanback.widget.GuidedAction;
33 
34 import com.android.settingslib.applications.ApplicationsState;
35 import com.android.tv.settings.R;
36 import com.android.tv.settings.device.apps.MoveAppActivity;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Map;
41 
42 public class BackupAppsStepFragment extends GuidedStepFragment implements
43         ApplicationsState.Callbacks {
44 
45     private static final int ACTION_NO_APPS = 0;
46     private static final int ACTION_MIGRATE_DATA = 1;
47     private static final int ACTION_BACKUP_APP_BASE = 100;
48 
49     private ApplicationsState mApplicationsState;
50     private ApplicationsState.Session mSession;
51 
52     private PackageManager mPackageManager;
53     private StorageManager mStorageManager;
54 
55     private String mVolumeId;
56     private ApplicationsState.AppFilter mAppFilter;
57 
58     private IconLoaderTask mIconLoaderTask;
59     private final Map<String, Drawable> mIconMap = new ArrayMap<>();
60 
61     private final List<ApplicationsState.AppEntry> mEntries = new ArrayList<>();
62 
newInstance(String volumeId)63     public static BackupAppsStepFragment newInstance(String volumeId) {
64         final BackupAppsStepFragment fragment = new BackupAppsStepFragment();
65         final Bundle b = new Bundle(1);
66         b.putString(VolumeInfo.EXTRA_VOLUME_ID, volumeId);
67         fragment.setArguments(b);
68         return fragment;
69     }
70 
71     @Override
onCreate(Bundle savedInstanceState)72     public void onCreate(Bundle savedInstanceState) {
73         // Need mPackageManager before onCreateActions, which is called from super.onCreate
74         mPackageManager = getActivity().getPackageManager();
75         mStorageManager = getActivity().getSystemService(StorageManager.class);
76 
77         mVolumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID);
78         final VolumeInfo info = mStorageManager.findVolumeById(mVolumeId);
79         if (info != null) {
80             mAppFilter = new ApplicationsState.VolumeFilter(info.getFsUuid());
81         } else {
82             if (!getFragmentManager().popBackStackImmediate()) {
83                 getActivity().finish();
84             }
85             mAppFilter = new ApplicationsState.AppFilter() {
86                 @Override
87                 public void init() {}
88 
89                 @Override
90                 public boolean filterApp(ApplicationsState.AppEntry info) {
91                     return false;
92                 }
93             };
94         }
95 
96         mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication());
97         mSession = mApplicationsState.newSession(this);
98 
99         super.onCreate(savedInstanceState);
100     }
101 
102     @Override
onResume()103     public void onResume() {
104         super.onResume();
105         mSession.onResume();
106         updateActions();
107     }
108 
109     @Override
onPause()110     public void onPause() {
111         super.onPause();
112         mSession.onPause();
113     }
114 
115     @Override
onDestroy()116     public void onDestroy() {
117         super.onDestroy();
118         mSession.onDestroy();
119     }
120 
121     @Override
onCreateGuidance(Bundle savedInstanceState)122     public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
123         final String title;
124         final VolumeInfo volumeInfo = mStorageManager.findVolumeById(mVolumeId);
125         final String volumeDesc = mStorageManager.getBestVolumeDescription(volumeInfo);
126         final String primaryStorageVolumeId =
127                 mPackageManager.getPrimaryStorageCurrentVolume().getId();
128         if (TextUtils.equals(primaryStorageVolumeId, volumeInfo.getId())) {
129             title = getString(R.string.storage_wizard_back_up_apps_and_data_title, volumeDesc);
130         } else {
131             title = getString(R.string.storage_wizard_back_up_apps_title, volumeDesc);
132         }
133         return new GuidanceStylist.Guidance(
134                 title,
135                 "",
136                 "",
137                 getActivity().getDrawable(R.drawable.ic_storage_132dp));
138     }
139 
140     @Override
onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)141     public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
142         final List<ApplicationsState.AppEntry> entries = mSession.rebuild(mAppFilter,
143                 ApplicationsState.ALPHA_COMPARATOR);
144         if (entries != null) {
145             actions.addAll(getAppActions(true, entries));
146         }
147     }
148 
getAppActions(boolean refreshIcons, List<ApplicationsState.AppEntry> entries)149     private List<GuidedAction> getAppActions(boolean refreshIcons,
150             List<ApplicationsState.AppEntry> entries) {
151 
152         final List<GuidedAction> actions = new ArrayList<>(entries.size() + 1);
153 
154         boolean showMigrate = false;
155         final VolumeInfo currentExternal = mPackageManager.getPrimaryStorageCurrentVolume();
156         // currentExternal will be null if the drive is not mounted. Don't offer the option to
157         // migrate if so.
158         if (currentExternal != null
159                 && TextUtils.equals(currentExternal.getId(), mVolumeId)) {
160             final List<VolumeInfo> candidates =
161                     mPackageManager.getPrimaryStorageCandidateVolumes();
162             for (final VolumeInfo candidate : candidates) {
163                 if (!TextUtils.equals(candidate.getId(), mVolumeId)) {
164                     showMigrate = true;
165                     break;
166                 }
167             }
168         }
169 
170         if (showMigrate) {
171             actions.add(new GuidedAction.Builder(getContext())
172                     .id(ACTION_MIGRATE_DATA)
173                     .title(R.string.storage_migrate_away)
174                     .build());
175         }
176 
177         int index = ACTION_BACKUP_APP_BASE;
178         for (final ApplicationsState.AppEntry entry : entries) {
179             final ApplicationInfo info = entry.info;
180             entry.ensureLabel(getActivity());
181             actions.add(new GuidedAction.Builder(getContext())
182                     .title(entry.label)
183                     .description(entry.sizeStr)
184                     .icon(mIconMap.get(info.packageName))
185                     .id(index++)
186                     .build());
187         }
188         mEntries.clear();
189         mEntries.addAll(entries);
190 
191         if (refreshIcons) {
192             if (mIconLoaderTask != null) {
193                 mIconLoaderTask.cancel(true);
194             }
195             mIconLoaderTask = new IconLoaderTask(entries);
196             mIconLoaderTask.execute();
197         }
198 
199         if (actions.size() == 0) {
200             actions.add(new GuidedAction.Builder(getContext())
201                     .id(ACTION_NO_APPS)
202                     .title(R.string.storage_no_apps)
203                     .build());
204         }
205         return actions;
206     }
207 
updateActions()208     private void updateActions() {
209         final List<ApplicationsState.AppEntry> entries = mSession.rebuild(mAppFilter,
210                 ApplicationsState.ALPHA_COMPARATOR);
211         if (entries != null) {
212             setActions(getAppActions(true, entries));
213         } else {
214             setActions(getAppActions(true, mEntries));
215         }
216     }
217 
218     @Override
onGuidedActionClicked(GuidedAction action)219     public void onGuidedActionClicked(GuidedAction action) {
220         final int actionId = (int) action.getId();
221         if (actionId == ACTION_MIGRATE_DATA) {
222             startActivity(MigrateStorageActivity.getLaunchIntent(getActivity(), mVolumeId, false));
223         } else if (actionId == ACTION_NO_APPS) {
224             if (!getFragmentManager().popBackStackImmediate()) {
225                 getActivity().finish();
226             }
227         } else if (actionId >= ACTION_BACKUP_APP_BASE
228                 && actionId < mEntries.size() + ACTION_BACKUP_APP_BASE) {
229             final ApplicationsState.AppEntry entry =
230                     mEntries.get(actionId - ACTION_BACKUP_APP_BASE);
231             entry.ensureLabel(getActivity());
232             startActivity(MoveAppActivity.getLaunchIntent(getActivity(), entry.info.packageName,
233                     entry.label));
234         } else {
235             throw new IllegalArgumentException("Unknown action " + action);
236         }
237     }
238 
239     @Override
onRunningStateChanged(boolean running)240     public void onRunningStateChanged(boolean running) {
241         updateActions();
242     }
243 
244     @Override
onPackageListChanged()245     public void onPackageListChanged() {
246         updateActions();
247     }
248 
249     @Override
onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps)250     public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
251         setActions(getAppActions(true, apps));
252     }
253 
254     @Override
onLauncherInfoChanged()255     public void onLauncherInfoChanged() {
256         updateActions();
257     }
258 
259     @Override
onLoadEntriesCompleted()260     public void onLoadEntriesCompleted() {
261         updateActions();
262     }
263 
264     @Override
onPackageIconChanged()265     public void onPackageIconChanged() {
266         updateActions();
267     }
268 
269     @Override
onPackageSizeChanged(String packageName)270     public void onPackageSizeChanged(String packageName) {
271         updateActions();
272     }
273 
274     @Override
onAllSizesComputed()275     public void onAllSizesComputed() {
276         updateActions();
277     }
278 
279     private class IconLoaderTask extends AsyncTask<Void, Void, Map<String, Drawable>> {
280         private final List<ApplicationsState.AppEntry> mEntries;
281 
IconLoaderTask(List<ApplicationsState.AppEntry> entries)282         public IconLoaderTask(List<ApplicationsState.AppEntry> entries) {
283             mEntries = entries;
284         }
285 
286         @Override
doInBackground(Void... params)287         protected Map<String, Drawable> doInBackground(Void... params) {
288             // NB: Java doesn't like parameterized generics in varargs
289             final Map<String, Drawable> result = new ArrayMap<>(mEntries.size());
290             for (final ApplicationsState.AppEntry entry : mEntries) {
291                 result.put(entry.info.packageName, mPackageManager.getApplicationIcon(entry.info));
292             }
293             return result;
294         }
295 
296         @Override
onPostExecute(Map<String, Drawable> stringDrawableMap)297         protected void onPostExecute(Map<String, Drawable> stringDrawableMap) {
298             mIconLoaderTask = null;
299             if (!isAdded()) {
300                 return;
301             }
302             mIconMap.putAll(stringDrawableMap);
303             setActions(getAppActions(false, mEntries));
304         }
305     }
306 
307 }
308