1 /* 2 * Copyright (C) 2019 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.car.settings.applications; 17 18 import android.os.Handler; 19 import android.os.storage.VolumeInfo; 20 21 import androidx.lifecycle.Lifecycle; 22 23 import com.android.car.settings.common.Logger; 24 import com.android.settingslib.applications.ApplicationsState; 25 26 import java.util.ArrayList; 27 import java.util.Comparator; 28 import java.util.HashSet; 29 import java.util.List; 30 import java.util.Set; 31 32 /** 33 * Class used to load the applications installed on the system with their metadata. 34 */ 35 // TODO: consolidate with AppEntryListManager. 36 public class ApplicationListItemManager implements ApplicationsState.Callbacks { 37 /** 38 * Callback that is called once the list of applications are loaded. 39 */ 40 public interface AppListItemListener { 41 /** 42 * Called when the data is successfully loaded from {@link ApplicationsState.Callbacks} and 43 * icon, title and summary are set for all the applications. 44 */ onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps)45 void onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps); 46 } 47 48 private static final Logger LOG = new Logger(ApplicationListItemManager.class); 49 private static final String APP_NAME_UNKNOWN = "APP NAME UNKNOWN"; 50 51 private final VolumeInfo mVolumeInfo; 52 private final Lifecycle mLifecycle; 53 private final ApplicationsState mAppState; 54 private final List<AppListItemListener> mAppListItemListeners = new ArrayList<>(); 55 private final Handler mHandler; 56 private final int mMillisecondUpdateInterval; 57 // Milliseconds that warnIfNotAllLoadedInTime method waits before comparing mAppsToLoad and 58 // mLoadedApps to log any apps that failed to load. 59 private final int mMaxAppLoadWaitInterval; 60 61 private ApplicationsState.Session mSession; 62 private ApplicationsState.AppFilter mAppFilter; 63 private Comparator<ApplicationsState.AppEntry> mAppEntryComparator; 64 // Contains all of the apps that we are expecting to load. 65 private Set<ApplicationsState.AppEntry> mAppsToLoad = new HashSet<>(); 66 // Contains all apps that have been successfully loaded. 67 private ArrayList<ApplicationsState.AppEntry> mLoadedApps = new ArrayList<>(); 68 69 // Indicates whether onRebuildComplete's throttling is off and it is ready to render updates. 70 // onRebuildComplete uses throttling to prevent it from being called too often, since the 71 // animation can be choppy if the refresh rate is too high. 72 private boolean mReadyToRenderUpdates = true; 73 // Parameter we use to call onRebuildComplete method when the throttling is off and we are 74 // "ReadyToRenderUpdates" again. 75 private ArrayList<ApplicationsState.AppEntry> mDeferredAppsToUpload; 76 ApplicationListItemManager(VolumeInfo volumeInfo, Lifecycle lifecycle, ApplicationsState appState, int millisecondUpdateInterval, int maxWaitIntervalToFinishLoading)77 public ApplicationListItemManager(VolumeInfo volumeInfo, Lifecycle lifecycle, 78 ApplicationsState appState, int millisecondUpdateInterval, 79 int maxWaitIntervalToFinishLoading) { 80 mVolumeInfo = volumeInfo; 81 mLifecycle = lifecycle; 82 mAppState = appState; 83 mHandler = new Handler(); 84 mMillisecondUpdateInterval = millisecondUpdateInterval; 85 mMaxAppLoadWaitInterval = maxWaitIntervalToFinishLoading; 86 } 87 88 /** 89 * Registers a listener that will be notified once the data is loaded. 90 */ registerListener(AppListItemListener appListItemListener)91 public void registerListener(AppListItemListener appListItemListener) { 92 if (!mAppListItemListeners.contains(appListItemListener) && appListItemListener != null) { 93 mAppListItemListeners.add(appListItemListener); 94 } 95 } 96 97 /** 98 * Unregisters the listener. 99 */ unregisterlistener(AppListItemListener appListItemListener)100 public void unregisterlistener(AppListItemListener appListItemListener) { 101 mAppListItemListeners.remove(appListItemListener); 102 } 103 104 /** 105 * Resumes the session and starts meauring app loading time on fragment start. 106 */ onFragmentStart()107 public void onFragmentStart() { 108 mSession.onResume(); 109 warnIfNotAllLoadedInTime(); 110 } 111 112 /** 113 * Pause the session on fragment stop. 114 */ onFragmentStop()115 public void onFragmentStop() { 116 mSession.onPause(); 117 } 118 119 /** 120 * Starts the new session and start loading the list of installed applications on the device. 121 * This list will be filtered out based on the {@link ApplicationsState.AppFilter} provided. 122 * Once the list is ready, {@link AppListItemListener#onDataLoaded} will be called. 123 * 124 * @param appFilter based on which the list of applications will be filtered before 125 * returning. 126 * @param appEntryComparator comparator based on which the application list will be sorted. 127 */ startLoading(ApplicationsState.AppFilter appFilter, Comparator<ApplicationsState.AppEntry> appEntryComparator)128 public void startLoading(ApplicationsState.AppFilter appFilter, 129 Comparator<ApplicationsState.AppEntry> appEntryComparator) { 130 if (mSession != null) { 131 LOG.w("Loading already started but restart attempted."); 132 return; // Prevent leaking sessions. 133 } 134 mAppFilter = appFilter; 135 mAppEntryComparator = appEntryComparator; 136 mSession = mAppState.newSession(this, mLifecycle); 137 } 138 139 /** 140 * Rebuilds the list of applications using the provided {@link ApplicationsState.AppFilter}. 141 * The filter will be used for all subsequent loading. Once the list is ready, {@link 142 * AppListItemListener#onDataLoaded} will be called. 143 */ rebuildWithFilter(ApplicationsState.AppFilter appFilter)144 public void rebuildWithFilter(ApplicationsState.AppFilter appFilter) { 145 mAppFilter = appFilter; 146 rebuild(); 147 } 148 149 @Override onPackageIconChanged()150 public void onPackageIconChanged() { 151 rebuild(); 152 } 153 154 @Override onPackageSizeChanged(String packageName)155 public void onPackageSizeChanged(String packageName) { 156 rebuild(); 157 } 158 159 @Override onAllSizesComputed()160 public void onAllSizesComputed() { 161 rebuild(); 162 } 163 164 @Override onLauncherInfoChanged()165 public void onLauncherInfoChanged() { 166 rebuild(); 167 } 168 169 @Override onLoadEntriesCompleted()170 public void onLoadEntriesCompleted() { 171 rebuild(); 172 } 173 174 @Override onRunningStateChanged(boolean running)175 public void onRunningStateChanged(boolean running) { 176 } 177 178 @Override onPackageListChanged()179 public void onPackageListChanged() { 180 rebuild(); 181 } 182 183 @Override onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps)184 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { 185 // Checking for apps.size prevents us from unnecessarily triggering throttling and blocking 186 // subsequent updates. 187 if (apps.size() == 0) { 188 return; 189 } 190 191 if (mReadyToRenderUpdates) { 192 mReadyToRenderUpdates = false; 193 mLoadedApps = new ArrayList<>(); 194 195 for (ApplicationsState.AppEntry app : apps) { 196 if (isLoaded(app)) { 197 mLoadedApps.add(app); 198 } 199 } 200 201 for (AppListItemListener appListItemListener : mAppListItemListeners) { 202 appListItemListener.onDataLoaded(mLoadedApps); 203 } 204 205 mHandler.postDelayed(() -> { 206 mReadyToRenderUpdates = true; 207 if (mDeferredAppsToUpload != null) { 208 onRebuildComplete(mDeferredAppsToUpload); 209 mDeferredAppsToUpload = null; 210 } 211 }, mMillisecondUpdateInterval); 212 } else { 213 mDeferredAppsToUpload = apps; 214 } 215 216 // Add all apps that are not already contained in mAppsToLoad Set, since we want it to be an 217 // exhaustive Set of all apps to be loaded. 218 mAppsToLoad.addAll(apps); 219 } 220 isLoaded(ApplicationsState.AppEntry app)221 private boolean isLoaded(ApplicationsState.AppEntry app) { 222 return app.label != null && app.sizeStr != null && app.icon != null; 223 } 224 warnIfNotAllLoadedInTime()225 private void warnIfNotAllLoadedInTime() { 226 mHandler.postDelayed(() -> { 227 if (mLoadedApps.size() < mAppsToLoad.size()) { 228 LOG.w("Expected to load " + mAppsToLoad.size() + " apps but only loaded " 229 + mLoadedApps.size()); 230 231 // Creating a copy to avoid state inconsistency. 232 Set<ApplicationsState.AppEntry> appsToLoadCopy = new HashSet(mAppsToLoad); 233 for (ApplicationsState.AppEntry loadedApp : mLoadedApps) { 234 appsToLoadCopy.remove(loadedApp); 235 } 236 237 for (ApplicationsState.AppEntry appEntry : appsToLoadCopy) { 238 String appName = appEntry.label == null ? APP_NAME_UNKNOWN : appEntry.label; 239 LOG.w("App failed to load: " + appName); 240 } 241 } 242 }, mMaxAppLoadWaitInterval); 243 } 244 getCompositeFilter(String volumeUuid)245 ApplicationsState.AppFilter getCompositeFilter(String volumeUuid) { 246 if (mAppFilter == null) { 247 return null; 248 } 249 ApplicationsState.AppFilter filter = new ApplicationsState.VolumeFilter(volumeUuid); 250 filter = new ApplicationsState.CompoundFilter(mAppFilter, filter); 251 return filter; 252 } 253 rebuild()254 private void rebuild() { 255 ApplicationsState.AppFilter filterObj = ApplicationsState.FILTER_EVERYTHING; 256 257 filterObj = new ApplicationsState.CompoundFilter(filterObj, 258 ApplicationsState.FILTER_NOT_HIDE); 259 ApplicationsState.AppFilter compositeFilter = getCompositeFilter(mVolumeInfo.getFsUuid()); 260 if (compositeFilter != null) { 261 filterObj = new ApplicationsState.CompoundFilter(filterObj, compositeFilter); 262 } 263 ApplicationsState.AppFilter finalFilterObj = filterObj; 264 mSession.rebuild(finalFilterObj, mAppEntryComparator, /* foreground= */ false); 265 } 266 } 267