• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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