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 17 package com.android.tv.settings.device.apps; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.pm.ApplicationInfo; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.os.SystemClock; 25 import android.support.annotation.NonNull; 26 import android.support.v17.preference.LeanbackPreferenceFragment; 27 import android.support.v7.preference.Preference; 28 import android.support.v7.preference.PreferenceCategory; 29 import android.support.v7.preference.PreferenceGroup; 30 import android.support.v7.preference.PreferenceScreen; 31 import android.text.TextUtils; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import android.util.Log; 35 36 import com.android.settingslib.applications.ApplicationsState; 37 import com.android.tv.settings.R; 38 39 import java.util.ArrayList; 40 import java.util.Map; 41 import java.util.Set; 42 43 public class AppsFragment extends LeanbackPreferenceFragment { 44 45 private static final String TAG = "AppsFragment"; 46 47 private ApplicationsState mApplicationsState; 48 private ApplicationsState.Session mSessionSystem; 49 private ApplicationsState.AppFilter mFilterSystem; 50 private ApplicationsState.Session mSessionDownloaded; 51 private ApplicationsState.AppFilter mFilterDownloaded; 52 53 private PreferenceGroup mSystemPreferenceGroup; 54 private PreferenceGroup mDownloadedPreferenceGroup; 55 56 private final Handler mHandler = new Handler(); 57 private final Map<PreferenceGroup, 58 ArrayList<ApplicationsState.AppEntry>> mUpdateMap = new ArrayMap<>(3); 59 private long mRunAt = Long.MIN_VALUE; 60 private final Runnable mUpdateRunnable = new Runnable() { 61 @Override 62 public void run() { 63 for (final PreferenceGroup group : mUpdateMap.keySet()) { 64 final ArrayList<ApplicationsState.AppEntry> entries = mUpdateMap.get(group); 65 updateAppListInternal(group, entries); 66 } 67 mUpdateMap.clear(); 68 mRunAt = 0; 69 } 70 }; 71 prepareArgs(Bundle b, String volumeUuid, String volumeName)72 public static void prepareArgs(Bundle b, String volumeUuid, String volumeName) { 73 b.putString(AppsActivity.EXTRA_VOLUME_UUID, volumeUuid); 74 b.putString(AppsActivity.EXTRA_VOLUME_NAME, volumeName); 75 } 76 newInstance(String volumeUuid, String volumeName)77 public static AppsFragment newInstance(String volumeUuid, String volumeName) { 78 final Bundle b = new Bundle(2); 79 prepareArgs(b, volumeUuid, volumeName); 80 final AppsFragment f = new AppsFragment(); 81 f.setArguments(b); 82 return f; 83 } 84 85 @Override onActivityCreated(Bundle savedInstanceState)86 public void onActivityCreated(Bundle savedInstanceState) { 87 super.onActivityCreated(savedInstanceState); 88 mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication()); 89 90 final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID); 91 final String volumeName = getArguments().getString(AppsActivity.EXTRA_VOLUME_NAME); 92 93 // The UUID of internal storage is null, so we check if there's a volume name to see if we 94 // should only be showing the apps on the internal storage or all apps. 95 if (!TextUtils.isEmpty(volumeUuid) || !TextUtils.isEmpty(volumeName)) { 96 ApplicationsState.AppFilter volumeFilter = 97 new ApplicationsState.VolumeFilter(volumeUuid); 98 99 mFilterSystem = 100 new ApplicationsState.CompoundFilter(FILTER_SYSTEM, volumeFilter); 101 mFilterDownloaded = 102 new ApplicationsState.CompoundFilter(FILTER_DOWNLOADED, volumeFilter); 103 } else { 104 mFilterSystem = FILTER_SYSTEM; 105 mFilterDownloaded = FILTER_DOWNLOADED; 106 } 107 108 mSessionSystem = mApplicationsState.newSession(new RowUpdateCallbacks() { 109 @Override 110 protected void doRebuild() { 111 rebuildSystem(); 112 } 113 114 @Override 115 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { 116 updateAppList(mSystemPreferenceGroup, apps); 117 } 118 }); 119 rebuildSystem(); 120 121 mSessionDownloaded = mApplicationsState.newSession(new RowUpdateCallbacks() { 122 @Override 123 protected void doRebuild() { 124 rebuildDownloaded(); 125 } 126 127 @Override 128 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { 129 updateAppList(mDownloadedPreferenceGroup, apps); 130 } 131 }); 132 rebuildDownloaded(); 133 134 } 135 136 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)137 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 138 final Context context = getPreferenceManager().getContext(); 139 140 final PreferenceScreen screen = 141 getPreferenceManager().createPreferenceScreen(context); 142 screen.setTitle(R.string.device_apps); 143 // TODO: show volume name somewhere? 144 145 final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID); 146 147 if (TextUtils.isEmpty(volumeUuid)) { 148 final Preference permissionsPreference = new Preference(context); 149 permissionsPreference.setKey("Permissions"); 150 permissionsPreference.setTitle(R.string.device_apps_permissions); 151 permissionsPreference.setIntent(new Intent(Intent.ACTION_MANAGE_PERMISSIONS)); 152 screen.addPreference(permissionsPreference); 153 } 154 mDownloadedPreferenceGroup = new PreferenceCategory(context); 155 mDownloadedPreferenceGroup.setKey("DownloadedPreferenceGroup"); 156 mDownloadedPreferenceGroup.setTitle(R.string.apps_downloaded); 157 mDownloadedPreferenceGroup.setOrderingAsAdded(false); 158 screen.addPreference(mDownloadedPreferenceGroup); 159 160 if (TextUtils.isEmpty(volumeUuid)) { 161 mSystemPreferenceGroup = new PreferenceCategory(context); 162 mSystemPreferenceGroup.setKey("SystemPreferenceGroup"); 163 mSystemPreferenceGroup.setTitle(R.string.apps_system); 164 mSystemPreferenceGroup.setOrderingAsAdded(false); 165 screen.addPreference(mSystemPreferenceGroup); 166 } 167 168 setPreferenceScreen(screen); 169 } 170 171 @Override onResume()172 public void onResume() { 173 super.onResume(); 174 mSessionSystem.resume(); 175 mSessionDownloaded.resume(); 176 } 177 178 @Override onPause()179 public void onPause() { 180 super.onPause(); 181 mSessionSystem.pause(); 182 mSessionDownloaded.pause(); 183 } 184 rebuildSystem()185 private void rebuildSystem() { 186 ArrayList<ApplicationsState.AppEntry> apps = 187 mSessionSystem.rebuild(mFilterSystem, ApplicationsState.ALPHA_COMPARATOR); 188 if (apps != null) { 189 updateAppList(mSystemPreferenceGroup, apps); 190 } 191 } 192 rebuildDownloaded()193 private void rebuildDownloaded() { 194 ArrayList<ApplicationsState.AppEntry> apps = 195 mSessionDownloaded.rebuild(mFilterDownloaded, ApplicationsState.ALPHA_COMPARATOR); 196 if (apps != null) { 197 updateAppList(mDownloadedPreferenceGroup, apps); 198 } 199 } 200 updateAppList(PreferenceGroup group, ArrayList<ApplicationsState.AppEntry> entries)201 private void updateAppList(PreferenceGroup group, 202 ArrayList<ApplicationsState.AppEntry> entries) { 203 if (group == null) { 204 Log.d(TAG, "Not updating list for null group"); 205 return; 206 } 207 mUpdateMap.put(group, entries); 208 209 // We can get spammed with updates, so coalesce them to reduce jank and flicker 210 if (mRunAt == Long.MIN_VALUE) { 211 // First run, no delay 212 mHandler.removeCallbacks(mUpdateRunnable); 213 mHandler.post(mUpdateRunnable); 214 } else { 215 if (mRunAt == 0) { 216 mRunAt = SystemClock.uptimeMillis() + 1000; 217 } 218 int delay = (int) (mRunAt - SystemClock.uptimeMillis()); 219 delay = delay < 0 ? 0 : delay; 220 221 mHandler.removeCallbacks(mUpdateRunnable); 222 mHandler.postDelayed(mUpdateRunnable, delay); 223 } 224 } 225 226 private void updateAppListInternal(PreferenceGroup group, 227 ArrayList<ApplicationsState.AppEntry> entries) { 228 if (entries != null) { 229 final Set<String> touched = new ArraySet<>(entries.size()); 230 for (final ApplicationsState.AppEntry entry : entries) { 231 final String packageName = entry.info.packageName; 232 Preference recycle = group.findPreference(packageName); 233 if (recycle == null) { 234 recycle = new Preference(getPreferenceManager().getContext()); 235 } 236 final Preference newPref = bindPreference(recycle, entry); 237 group.addPreference(newPref); 238 touched.add(packageName); 239 } 240 for (int i = 0; i < group.getPreferenceCount();) { 241 final Preference pref = group.getPreference(i); 242 if (touched.contains(pref.getKey())) { 243 i++; 244 } else { 245 group.removePreference(pref); 246 } 247 } 248 } 249 } 250 251 /** 252 * Creates or updates a preference according to an {@link ApplicationsState.AppEntry} object 253 * @param preference If non-null, updates this preference object, otherwise creates a new one 254 * @param entry Info to populate preference 255 * @return Updated preference entry 256 */ 257 private Preference bindPreference(@NonNull Preference preference, 258 ApplicationsState.AppEntry entry) { 259 preference.setKey(entry.info.packageName); 260 entry.ensureLabel(getContext()); 261 preference.setTitle(entry.label); 262 preference.setSummary(entry.sizeStr); 263 preference.setFragment(AppManagementFragment.class.getName()); 264 AppManagementFragment.prepareArgs(preference.getExtras(), entry.info.packageName); 265 preference.setIcon(entry.icon); 266 return preference; 267 } 268 269 private abstract class RowUpdateCallbacks implements ApplicationsState.Callbacks { 270 271 protected abstract void doRebuild(); 272 273 @Override 274 public void onRunningStateChanged(boolean running) { 275 doRebuild(); 276 } 277 278 @Override 279 public void onPackageListChanged() { 280 doRebuild(); 281 } 282 283 @Override 284 public void onPackageIconChanged() { 285 doRebuild(); 286 } 287 288 @Override 289 public void onPackageSizeChanged(String packageName) { 290 doRebuild(); 291 } 292 293 @Override 294 public void onAllSizesComputed() { 295 doRebuild(); 296 } 297 298 @Override 299 public void onLauncherInfoChanged() { 300 doRebuild(); 301 } 302 303 @Override 304 public void onLoadEntriesCompleted() { 305 doRebuild(); 306 } 307 } 308 309 private static final ApplicationsState.AppFilter FILTER_SYSTEM = 310 new ApplicationsState.AppFilter() { 311 312 @Override 313 public void init() {} 314 315 @Override 316 public boolean filterApp(ApplicationsState.AppEntry info) { 317 return (info.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 318 } 319 }; 320 321 private static final ApplicationsState.AppFilter FILTER_DOWNLOADED = 322 new ApplicationsState.AppFilter() { 323 324 @Override 325 public void init() {} 326 327 @Override 328 public boolean filterApp(ApplicationsState.AppEntry info) { 329 return (info.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0; 330 } 331 }; 332 333 } 334