1/*
2 * Copyright (C) 2018 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 */
16package com.android.tv.settings.device.apps;
17
18import android.content.pm.PackageManager;
19import android.os.Bundle;
20import android.os.Handler;
21import android.os.SystemClock;
22import android.support.annotation.NonNull;
23import android.support.v7.preference.Preference;
24import android.support.v7.preference.PreferenceGroup;
25import android.text.TextUtils;
26import android.util.ArrayMap;
27import android.util.ArraySet;
28import android.util.Log;
29
30import com.android.internal.logging.nano.MetricsProto;
31import com.android.settingslib.applications.ApplicationsState;
32import com.android.tv.settings.R;
33import com.android.tv.settings.SettingsPreferenceFragment;
34
35import java.util.ArrayList;
36import java.util.Map;
37import java.util.Set;
38
39/**
40 * Fragment for listing and managing all apps on the device.
41 */
42public class AllAppsFragment extends SettingsPreferenceFragment implements
43        Preference.OnPreferenceClickListener {
44
45    private static final String TAG = "AllAppsFragment";
46    private static final String KEY_SHOW_OTHER_APPS = "ShowOtherApps";
47
48    private static final @ApplicationsState.SessionFlags int SESSION_FLAGS =
49            ApplicationsState.FLAG_SESSION_REQUEST_HOME_APP
50            | ApplicationsState.FLAG_SESSION_REQUEST_ICONS
51            | ApplicationsState.FLAG_SESSION_REQUEST_SIZES
52            | ApplicationsState.FLAG_SESSION_REQUEST_LEANBACK_LAUNCHER;
53
54    private ApplicationsState mApplicationsState;
55    private ApplicationsState.Session mSessionInstalled;
56    private ApplicationsState.AppFilter mFilterInstalled;
57    private ApplicationsState.Session mSessionDisabled;
58    private ApplicationsState.AppFilter mFilterDisabled;
59    private ApplicationsState.Session mSessionOther;
60    private ApplicationsState.AppFilter mFilterOther;
61
62    private PreferenceGroup mInstalledPreferenceGroup;
63    private PreferenceGroup mDisabledPreferenceGroup;
64    private PreferenceGroup mOtherPreferenceGroup;
65    private Preference mShowOtherApps;
66
67    private final Handler mHandler = new Handler();
68    private final Map<PreferenceGroup,
69            ArrayList<ApplicationsState.AppEntry>> mUpdateMap = new ArrayMap<>(3);
70    private long mRunAt = Long.MIN_VALUE;
71    private final Runnable mUpdateRunnable = new Runnable() {
72        @Override
73        public void run() {
74            for (final PreferenceGroup group : mUpdateMap.keySet()) {
75                final ArrayList<ApplicationsState.AppEntry> entries = mUpdateMap.get(group);
76                updateAppListInternal(group, entries);
77            }
78            mUpdateMap.clear();
79            mRunAt = 0;
80        }
81    };
82
83    /** Prepares arguments for the fragment. */
84    public static void prepareArgs(Bundle b, String volumeUuid, String volumeName) {
85        b.putString(AppsActivity.EXTRA_VOLUME_UUID, volumeUuid);
86        b.putString(AppsActivity.EXTRA_VOLUME_NAME, volumeName);
87    }
88
89    /** Creates a new instance of the fragment. */
90    public static AllAppsFragment newInstance(String volumeUuid, String volumeName) {
91        final Bundle b = new Bundle(2);
92        prepareArgs(b, volumeUuid, volumeName);
93        final AllAppsFragment f = new AllAppsFragment();
94        f.setArguments(b);
95        return f;
96    }
97
98    @Override
99    public void onActivityCreated(Bundle savedInstanceState) {
100        super.onActivityCreated(savedInstanceState);
101        mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication());
102
103        final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
104        final String volumeName = getArguments().getString(AppsActivity.EXTRA_VOLUME_NAME);
105
106        // The UUID of internal storage is null, so we check if there's a volume name to see if we
107        // should only be showing the apps on the internal storage or all apps.
108        if (!TextUtils.isEmpty(volumeUuid) || !TextUtils.isEmpty(volumeName)) {
109            ApplicationsState.AppFilter volumeFilter =
110                    new ApplicationsState.VolumeFilter(volumeUuid);
111
112            mFilterInstalled =
113                    new ApplicationsState.CompoundFilter(FILTER_INSTALLED, volumeFilter);
114            mFilterDisabled =
115                    new ApplicationsState.CompoundFilter(FILTER_DISABLED, volumeFilter);
116            mFilterOther =
117                    new ApplicationsState.CompoundFilter(FILTER_OTHER, volumeFilter);
118        } else {
119            mFilterInstalled = FILTER_INSTALLED;
120            mFilterDisabled = FILTER_DISABLED;
121            mFilterOther = FILTER_OTHER;
122        }
123
124        mSessionInstalled = mApplicationsState.newSession(new RowUpdateCallbacks() {
125            @Override
126            protected void doRebuild() {
127                rebuildInstalled();
128            }
129
130            @Override
131            public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
132                updateAppList(mInstalledPreferenceGroup, apps);
133            }
134        }, getLifecycle());
135        mSessionInstalled.setSessionFlags(SESSION_FLAGS);
136
137        mSessionDisabled = mApplicationsState.newSession(new RowUpdateCallbacks() {
138            @Override
139            protected void doRebuild() {
140                rebuildDisabled();
141            }
142
143            @Override
144            public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
145                updateAppList(mDisabledPreferenceGroup, apps);
146            }
147        }, getLifecycle());
148        mSessionDisabled.setSessionFlags(SESSION_FLAGS);
149
150        mSessionOther = mApplicationsState.newSession(new RowUpdateCallbacks() {
151            @Override
152            protected void doRebuild() {
153                if (!mShowOtherApps.isVisible()) {
154                    rebuildOther();
155                }
156            }
157
158            @Override
159            public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
160                updateAppList(mOtherPreferenceGroup, apps);
161            }
162        }, getLifecycle());
163        mSessionOther.setSessionFlags(SESSION_FLAGS);
164
165
166        rebuildInstalled();
167        rebuildDisabled();
168    }
169
170    @Override
171    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
172        setPreferencesFromResource(R.xml.all_apps, null);
173        mInstalledPreferenceGroup = (PreferenceGroup) findPreference("InstalledPreferenceGroup");
174        mDisabledPreferenceGroup = (PreferenceGroup) findPreference("DisabledPreferenceGroup");
175        mOtherPreferenceGroup = (PreferenceGroup) findPreference("OtherPreferenceGroup");
176        mOtherPreferenceGroup.setVisible(false);
177        mShowOtherApps = findPreference(KEY_SHOW_OTHER_APPS);
178        mShowOtherApps.setOnPreferenceClickListener(this);
179        final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
180        mShowOtherApps.setVisible(TextUtils.isEmpty(volumeUuid));
181    }
182
183    private void rebuildInstalled() {
184        ArrayList<ApplicationsState.AppEntry> apps =
185                mSessionInstalled.rebuild(mFilterInstalled, ApplicationsState.ALPHA_COMPARATOR);
186        if (apps != null) {
187            updateAppList(mInstalledPreferenceGroup, apps);
188        }
189    }
190
191    private void rebuildDisabled() {
192        ArrayList<ApplicationsState.AppEntry> apps =
193                mSessionDisabled.rebuild(mFilterDisabled, ApplicationsState.ALPHA_COMPARATOR);
194        if (apps != null) {
195            updateAppList(mDisabledPreferenceGroup, apps);
196        }
197    }
198
199    private void rebuildOther() {
200        ArrayList<ApplicationsState.AppEntry> apps =
201                mSessionOther.rebuild(mFilterOther, ApplicationsState.ALPHA_COMPARATOR);
202        if (apps != null) {
203            updateAppList(mOtherPreferenceGroup, apps);
204        }
205    }
206
207    private void updateAppList(PreferenceGroup group,
208            ArrayList<ApplicationsState.AppEntry> entries) {
209        if (group == null) {
210            Log.d(TAG, "Not updating list for null group");
211            return;
212        }
213        mUpdateMap.put(group, entries);
214
215        // We can get spammed with updates, so coalesce them to reduce jank and flicker
216        if (mRunAt == Long.MIN_VALUE) {
217            // First run, no delay
218            mHandler.removeCallbacks(mUpdateRunnable);
219            mHandler.post(mUpdateRunnable);
220        } else {
221            if (mRunAt == 0) {
222                mRunAt = SystemClock.uptimeMillis() + 1000;
223            }
224            int delay = (int) (mRunAt - SystemClock.uptimeMillis());
225            delay = delay < 0 ? 0 : delay;
226
227            mHandler.removeCallbacks(mUpdateRunnable);
228            mHandler.postDelayed(mUpdateRunnable, delay);
229        }
230    }
231
232    private void updateAppListInternal(PreferenceGroup group,
233            ArrayList<ApplicationsState.AppEntry> entries) {
234        if (entries != null) {
235            final Set<String> touched = new ArraySet<>(entries.size());
236            for (final ApplicationsState.AppEntry entry : entries) {
237                final String packageName = entry.info.packageName;
238                Preference recycle = group.findPreference(packageName);
239                if (recycle == null) {
240                    recycle = new Preference(getPreferenceManager().getContext());
241                }
242                final Preference newPref = bindPreference(recycle, entry);
243                group.addPreference(newPref);
244                touched.add(packageName);
245            }
246            for (int i = 0; i < group.getPreferenceCount();) {
247                final Preference pref = group.getPreference(i);
248                if (touched.contains(pref.getKey())) {
249                    i++;
250                } else {
251                    group.removePreference(pref);
252                }
253            }
254        }
255        mDisabledPreferenceGroup.setVisible(mDisabledPreferenceGroup.getPreferenceCount() > 0);
256    }
257
258    /**
259     * Creates or updates a preference according to an {@link ApplicationsState.AppEntry} object
260     * @param preference If non-null, updates this preference object, otherwise creates a new one
261     * @param entry Info to populate preference
262     * @return Updated preference entry
263     */
264    private Preference bindPreference(@NonNull Preference preference,
265            ApplicationsState.AppEntry entry) {
266        preference.setKey(entry.info.packageName);
267        entry.ensureLabel(getContext());
268        preference.setTitle(entry.label);
269        preference.setSummary(entry.sizeStr);
270        preference.setFragment(AppManagementFragment.class.getName());
271        AppManagementFragment.prepareArgs(preference.getExtras(), entry.info.packageName);
272        preference.setIcon(entry.icon);
273        return preference;
274    }
275
276    @Override
277    public boolean onPreferenceClick(Preference preference) {
278        if  (KEY_SHOW_OTHER_APPS.equals(preference.getKey())) {
279            showOtherApps();
280            return true;
281        }
282        return false;
283    }
284
285    private void showOtherApps() {
286        mShowOtherApps.setVisible(false);
287        mOtherPreferenceGroup.setVisible(true);
288        rebuildOther();
289    }
290
291    private abstract class RowUpdateCallbacks implements ApplicationsState.Callbacks {
292
293        protected abstract void doRebuild();
294
295        @Override
296        public void onRunningStateChanged(boolean running) {
297            doRebuild();
298        }
299
300        @Override
301        public void onPackageListChanged() {
302            doRebuild();
303        }
304
305        @Override
306        public void onPackageIconChanged() {
307            doRebuild();
308        }
309
310        @Override
311        public void onPackageSizeChanged(String packageName) {
312            doRebuild();
313        }
314
315        @Override
316        public void onAllSizesComputed() {
317            doRebuild();
318        }
319
320        @Override
321        public void onLauncherInfoChanged() {
322            doRebuild();
323        }
324
325        @Override
326        public void onLoadEntriesCompleted() {
327            doRebuild();
328        }
329    }
330
331    private static final ApplicationsState.AppFilter FILTER_INSTALLED =
332            new ApplicationsState.AppFilter() {
333
334                @Override
335                public void init() {}
336
337                @Override
338                public boolean filterApp(ApplicationsState.AppEntry info) {
339                    return !FILTER_DISABLED.filterApp(info)
340                            && info.info != null
341                            && info.info.enabled
342                            && info.hasLauncherEntry
343                            && info.launcherEntryEnabled;
344                }
345            };
346
347    private static final ApplicationsState.AppFilter FILTER_DISABLED =
348            new ApplicationsState.AppFilter() {
349
350                @Override
351                public void init() {
352                }
353
354                @Override
355                public boolean filterApp(ApplicationsState.AppEntry info) {
356                    return info.info != null
357                            && (info.info.enabledSetting
358                            == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
359                            || info.info.enabledSetting
360                            == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
361                            || (info.info.enabledSetting
362                            == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
363                            && !info.info.enabled));
364                }
365            };
366
367    private static final ApplicationsState.AppFilter FILTER_OTHER =
368            new ApplicationsState.AppFilter() {
369
370                @Override
371                public void init() {}
372
373                @Override
374                public boolean filterApp(ApplicationsState.AppEntry info) {
375                    return !FILTER_INSTALLED.filterApp(info) && !FILTER_DISABLED.filterApp(info);
376                }
377            };
378
379    @Override
380    public int getMetricsCategory() {
381        return MetricsProto.MetricsEvent.MANAGE_APPLICATIONS;
382    }
383}
384