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