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
17package com.android.tv.settings.device.storage;
18
19import android.content.pm.ApplicationInfo;
20import android.content.pm.PackageManager;
21import android.graphics.drawable.Drawable;
22import android.os.AsyncTask;
23import android.os.Bundle;
24import android.os.storage.StorageManager;
25import android.os.storage.VolumeInfo;
26import android.support.annotation.NonNull;
27import android.support.v17.leanback.app.GuidedStepFragment;
28import android.support.v17.leanback.widget.GuidanceStylist;
29import android.support.v17.leanback.widget.GuidedAction;
30import android.text.TextUtils;
31import android.util.ArrayMap;
32
33import com.android.settingslib.applications.ApplicationsState;
34import com.android.tv.settings.R;
35import com.android.tv.settings.device.apps.AppInfo;
36import com.android.tv.settings.device.apps.MoveAppActivity;
37
38import java.util.ArrayList;
39import java.util.List;
40import java.util.Map;
41
42public 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
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
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
103    public void onResume() {
104        super.onResume();
105        mSession.resume();
106        updateActions();
107    }
108
109    @Override
110    public void onPause() {
111        super.onPause();
112        mSession.pause();
113    }
114
115    @Override
116    public @NonNull GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
117        final String title;
118        final VolumeInfo volumeInfo = mStorageManager.findVolumeById(mVolumeId);
119        final String volumeDesc = mStorageManager.getBestVolumeDescription(volumeInfo);
120        final String primaryStorageVolumeId =
121                mPackageManager.getPrimaryStorageCurrentVolume().getId();
122        if (TextUtils.equals(primaryStorageVolumeId, volumeInfo.getId())) {
123            title = getString(R.string.storage_wizard_back_up_apps_and_data_title, volumeDesc);
124        } else {
125            title = getString(R.string.storage_wizard_back_up_apps_title, volumeDesc);
126        }
127        return new GuidanceStylist.Guidance(
128                title,
129                "",
130                "",
131                getActivity().getDrawable(R.drawable.ic_storage_132dp));
132    }
133
134    @Override
135    public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
136        final List<ApplicationsState.AppEntry> entries = mSession.rebuild(mAppFilter,
137                ApplicationsState.ALPHA_COMPARATOR);
138        if (entries != null) {
139            actions.addAll(getAppActions(true, entries));
140        }
141    }
142
143    private List<GuidedAction> getAppActions(boolean refreshIcons,
144            List<ApplicationsState.AppEntry> entries) {
145
146        final List<GuidedAction> actions = new ArrayList<>(entries.size() + 1);
147
148        boolean showMigrate = false;
149        final VolumeInfo currentExternal = mPackageManager.getPrimaryStorageCurrentVolume();
150        // currentExternal will be null if the drive is not mounted. Don't offer the option to
151        // migrate if so.
152        if (currentExternal != null
153                && TextUtils.equals(currentExternal.getId(), mVolumeId)) {
154            final List<VolumeInfo> candidates =
155                    mPackageManager.getPrimaryStorageCandidateVolumes();
156            for (final VolumeInfo candidate : candidates) {
157                if (!TextUtils.equals(candidate.getId(), mVolumeId)) {
158                    showMigrate = true;
159                    break;
160                }
161            }
162        }
163
164        if (showMigrate) {
165            actions.add(new GuidedAction.Builder(getContext())
166                    .id(ACTION_MIGRATE_DATA)
167                    .title(R.string.storage_migrate_away)
168                    .build());
169        }
170
171        int index = ACTION_BACKUP_APP_BASE;
172        for (final ApplicationsState.AppEntry entry : entries) {
173            final ApplicationInfo info = entry.info;
174            final AppInfo appInfo = new AppInfo(getActivity(), entry);
175            actions.add(new GuidedAction.Builder(getContext())
176                    .title(appInfo.getName())
177                    .description(appInfo.getSize())
178                    .icon(mIconMap.get(info.packageName))
179                    .id(index++)
180                    .build());
181        }
182        mEntries.clear();
183        mEntries.addAll(entries);
184
185        if (refreshIcons) {
186            if (mIconLoaderTask != null) {
187                mIconLoaderTask.cancel(true);
188            }
189            mIconLoaderTask = new IconLoaderTask(entries);
190            mIconLoaderTask.execute();
191        }
192
193        if (actions.size() == 0) {
194            actions.add(new GuidedAction.Builder(getContext())
195                    .id(ACTION_NO_APPS)
196                    .title(R.string.storage_no_apps)
197                    .build());
198        }
199        return actions;
200    }
201
202    private void updateActions() {
203        final List<ApplicationsState.AppEntry> entries = mSession.rebuild(mAppFilter,
204                ApplicationsState.ALPHA_COMPARATOR);
205        if (entries != null) {
206            setActions(getAppActions(true, entries));
207        } else {
208            setActions(getAppActions(true, mEntries));
209        }
210    }
211
212    @Override
213    public void onGuidedActionClicked(GuidedAction action) {
214        final int actionId = (int) action.getId();
215        if (actionId == ACTION_MIGRATE_DATA) {
216            startActivity(MigrateStorageActivity.getLaunchIntent(getActivity(), mVolumeId, false));
217        } else if (actionId == ACTION_NO_APPS) {
218            if (!getFragmentManager().popBackStackImmediate()) {
219                getActivity().finish();
220            }
221        } else if (actionId >= ACTION_BACKUP_APP_BASE
222                && actionId < mEntries.size() + ACTION_BACKUP_APP_BASE) {
223            final ApplicationsState.AppEntry entry =
224                    mEntries.get(actionId - ACTION_BACKUP_APP_BASE);
225            final AppInfo appInfo = new AppInfo(getActivity(), entry);
226
227            startActivity(MoveAppActivity.getLaunchIntent(getActivity(), entry.info.packageName,
228                    appInfo.getName()));
229        } else {
230            throw new IllegalArgumentException("Unknown action " + action);
231        }
232    }
233
234    @Override
235    public void onRunningStateChanged(boolean running) {
236        updateActions();
237    }
238
239    @Override
240    public void onPackageListChanged() {
241        updateActions();
242    }
243
244    @Override
245    public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
246        setActions(getAppActions(true, apps));
247    }
248
249    @Override
250    public void onLauncherInfoChanged() {
251        updateActions();
252    }
253
254    @Override
255    public void onLoadEntriesCompleted() {
256        updateActions();
257    }
258
259    @Override
260    public void onPackageIconChanged() {
261        updateActions();
262    }
263
264    @Override
265    public void onPackageSizeChanged(String packageName) {
266        updateActions();
267    }
268
269    @Override
270    public void onAllSizesComputed() {
271        updateActions();
272    }
273
274    private class IconLoaderTask extends AsyncTask<Void, Void, Map<String, Drawable>> {
275        private final List<ApplicationsState.AppEntry> mEntries;
276
277        public IconLoaderTask(List<ApplicationsState.AppEntry> entries) {
278            mEntries = entries;
279        }
280
281        @Override
282        protected Map<String, Drawable> doInBackground(Void... params) {
283            // NB: Java doesn't like parameterized generics in varargs
284            final Map<String, Drawable> result = new ArrayMap<>(mEntries.size());
285            for (final ApplicationsState.AppEntry entry : mEntries) {
286                result.put(entry.info.packageName, mPackageManager.getApplicationIcon(entry.info));
287            }
288            return result;
289        }
290
291        @Override
292        protected void onPostExecute(Map<String, Drawable> stringDrawableMap) {
293            mIconLoaderTask = null;
294            if (!isAdded()) {
295                return;
296            }
297            mIconMap.putAll(stringDrawableMap);
298            setActions(getAppActions(false, mEntries));
299        }
300    }
301
302}
303