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