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