1 /*
2  * Copyright (C) 2023 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.car.carlauncher.apporder;
18 
19 import android.content.ComponentName;
20 import android.content.Intent;
21 
22 import androidx.annotation.VisibleForTesting;
23 import androidx.lifecycle.MutableLiveData;
24 
25 import com.android.car.carlauncher.AppItem;
26 import com.android.car.carlauncher.AppLauncherUtils;
27 import com.android.car.carlauncher.AppMetaData;
28 import com.android.car.carlauncher.LauncherItem;
29 import com.android.car.carlauncher.LauncherItemMessageHelper;
30 import com.android.car.carlauncher.LauncherItemProto.LauncherItemListMessage;
31 import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage;
32 import com.android.car.carlauncher.datastore.DataSourceController;
33 import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource;
34 
35 import java.io.File;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 
45 /**
46  * Controller that manages the ordering of the app items in app grid.
47  */
48 public class AppOrderController implements DataSourceController {
49     // file name holding the user customized app order
50     public static final String ORDER_FILE_NAME = "order.data";
51     private final LauncherItemMessageHelper mItemHelper = new LauncherItemMessageHelper();
52     // The app order of launcher items displayed to users
53     private final MutableLiveData<List<LauncherItem>> mCurrentAppList;
54     private final Map<ComponentName, LauncherItem> mLauncherItemMap = new HashMap<>();
55     private final List<ComponentName> mProtoComponentNames = new ArrayList<>();
56     private final List<LauncherItem> mDefaultOrder;
57     private final List<LauncherItem> mCustomizedOrder;
58     private final LauncherItemListSource mDataSource;
59     private boolean mPlatformAppListLoaded;
60     private boolean mCustomAppOrderFetched;
61     private boolean mIsUserCustomized;
62 
AppOrderController(File dataFileDirectory)63     public AppOrderController(File dataFileDirectory) {
64         this(/* dataSource */ new LauncherItemListSource(dataFileDirectory, ORDER_FILE_NAME),
65                 /* appList */ new MutableLiveData<>(new ArrayList<>()),
66                 /* defaultOrder */ new ArrayList<>(),
67                 /* customizedOrder*/ new ArrayList<>());
68     }
69 
AppOrderController(LauncherItemListSource dataSource, MutableLiveData<List<LauncherItem>> appList, List<LauncherItem> defaultOrder, List<LauncherItem> customizedOrder)70     public AppOrderController(LauncherItemListSource dataSource,
71             MutableLiveData<List<LauncherItem>> appList, List<LauncherItem> defaultOrder,
72             List<LauncherItem> customizedOrder) {
73         mDataSource = dataSource;
74         mCurrentAppList = appList;
75         mDefaultOrder = defaultOrder;
76         mCustomizedOrder = customizedOrder;
77     }
78 
79     @Override
checkDataSourceExists()80     public boolean checkDataSourceExists() {
81         return mDataSource.exists();
82     }
83 
getAppOrderObservable()84     public MutableLiveData<List<LauncherItem>> getAppOrderObservable() {
85         return mCurrentAppList;
86     }
87 
88     /**
89      * Loads the full app list to be displayed in the app grid.
90      */
loadAppListFromPlatform(Map<ComponentName, LauncherItem> launcherItemsMap, List<LauncherItem> defaultItemOrder)91     public void loadAppListFromPlatform(Map<ComponentName, LauncherItem> launcherItemsMap,
92             List<LauncherItem> defaultItemOrder) {
93         mDefaultOrder.clear();
94         mDefaultOrder.addAll(defaultItemOrder);
95         mLauncherItemMap.clear();
96         mLauncherItemMap.putAll(launcherItemsMap);
97         mPlatformAppListLoaded = true;
98         maybePublishAppList();
99     }
100 
101     /**
102      * Loads any preexisting app order from the proto datastore on disk.
103      */
loadAppOrderFromFile()104     public void loadAppOrderFromFile() {
105         // handle the app order reset case, where the proto file is removed from file system
106         maybeHandleAppOrderReset();
107         mProtoComponentNames.clear();
108         List<LauncherItemMessage> protoItemMessage = mItemHelper.getSortedList(
109                 mDataSource.readFromFile());
110         if (!protoItemMessage.isEmpty()) {
111             mIsUserCustomized = true;
112             for (LauncherItemMessage itemMessage : protoItemMessage) {
113                 ComponentName itemComponent = new ComponentName(
114                         itemMessage.getPackageName(), itemMessage.getClassName());
115                 mProtoComponentNames.add(itemComponent);
116             }
117         }
118         mCustomAppOrderFetched = true;
119         maybePublishAppList();
120     }
121 
122     @VisibleForTesting
maybeHandleAppOrderReset()123     void maybeHandleAppOrderReset() {
124         if (!checkDataSourceExists()) {
125             mIsUserCustomized = false;
126             mCustomizedOrder.clear();
127         }
128     }
129 
130     /**
131      * Combine the proto order read from proto with any additional apps read from the platform, then
132      * publish the new list to user interface.
133      *
134      * Prior to publishing the app list to the LiveData (and subsequently to the UI), both (1) the
135      * default platform mapping and (2) user customized order must be read into memory. These
136      * pre-fetch methods may be executed on different threads, so we should only publish the final
137      * ordering when both steps have completed.
138      */
139     @VisibleForTesting
maybePublishAppList()140     void maybePublishAppList() {
141         if (!appsDataLoadingCompleted()) {
142             return;
143         }
144         // app names found in order proto file will be displayed first
145         mCustomizedOrder.clear();
146         List<LauncherItem> customOrder = new ArrayList<>();
147         Set<ComponentName> namesFoundInProto = new HashSet<>();
148         for (ComponentName name: mProtoComponentNames) {
149             if (mLauncherItemMap.containsKey(name)) {
150                 customOrder.add(mLauncherItemMap.get(name));
151                 namesFoundInProto.add(name);
152             }
153         }
154         mCustomizedOrder.addAll(customOrder);
155         if (shouldUseCustomOrder()) {
156             // new apps from platform not found in proto will be added to the end
157             mCustomizedOrder.clear();
158             List<ComponentName> newPlatformApps = mLauncherItemMap.keySet()
159                     .stream()
160                     .filter(element -> !namesFoundInProto.contains(element))
161                     .collect(Collectors.toList());
162             if (!newPlatformApps.isEmpty()) {
163                 Collections.sort(newPlatformApps);
164                 for (ComponentName newAppName: newPlatformApps) {
165                     customOrder.add(mLauncherItemMap.get(newAppName));
166                 }
167             }
168             mCustomizedOrder.addAll(customOrder);
169             mCurrentAppList.postValue(customOrder);
170         } else {
171             mCurrentAppList.postValue(mDefaultOrder);
172             mCustomizedOrder.clear();
173         }
174         // reset apps data loading flags
175         mPlatformAppListLoaded = mCustomAppOrderFetched = false;
176     }
177 
178     @VisibleForTesting
appsDataLoadingCompleted()179     boolean appsDataLoadingCompleted() {
180         return mPlatformAppListLoaded && mCustomAppOrderFetched;
181     }
182 
183     @VisibleForTesting
shouldUseCustomOrder()184     boolean shouldUseCustomOrder() {
185         return mIsUserCustomized && mCustomizedOrder.size() != 0;
186     }
187 
188     /**
189      * Persistently writes the current in memory app order into disk.
190      */
handleAppListChange()191     public void handleAppListChange() {
192         if (mIsUserCustomized) {
193             List<LauncherItem> currentItems = mCurrentAppList.getValue();
194             List<LauncherItemMessage> msgList = new ArrayList<LauncherItemMessage>();
195             for (int i = 0; i < currentItems.size(); i++) {
196                 msgList.add(currentItems.get(i).convertToMessage(i, -1));
197             }
198             LauncherItemListMessage appOrderListMessage = mItemHelper.convertToMessage(msgList);
199             mDataSource.writeToFileInBackgroundThread(appOrderListMessage);
200         }
201     }
202 
203     /**
204      * Move an app to a specified index and post the value to LiveData.
205      */
setAppPosition(int position, AppMetaData app)206     public void setAppPosition(int position, AppMetaData app) {
207         List<LauncherItem> current = mCurrentAppList.getValue();
208         LauncherItem item = mLauncherItemMap.get(app.getComponentName());
209         if (current != null && current.size() != 0 && position < current.size() && item != null) {
210             mIsUserCustomized = true;
211             current.remove(item);
212             current.add(position, item);
213             mCurrentAppList.postValue(current);
214         }
215     }
216 
217     /**
218      * Handles the incoming mirroring intent from ViewModel.
219      *
220      * Update an AppItem's AppMetaData isMirroring state and its launch callback then post the
221      * updated to LiveData.
222      */
updateMirroringItem(String packageName, Intent mirroringIntent)223     public void updateMirroringItem(String packageName, Intent mirroringIntent) {
224         List<LauncherItem> launcherList = mCurrentAppList.getValue();
225         if (launcherList == null) {
226             return;
227         }
228         List<LauncherItem> launcherListCopy = new ArrayList<>();
229         for (LauncherItem item : launcherList) {
230             if (item instanceof AppItem) {
231                 // TODO (b/272796126): move deep copying to inside DiffUtil
232                 AppMetaData metaData = ((AppItem) item).getAppMetaData();
233                 if (item.getPackageName().equals(packageName)) {
234                     launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(),
235                             item.getDisplayName(), new AppMetaData(metaData.getDisplayName(),
236                             metaData.getComponentName(), metaData.getIcon(),
237                             metaData.getIsDistractionOptimized(), /* isMirroring= */ true,
238                             metaData.getIsDisabledByTos(),
239                                     contextArg ->
240                                             AppLauncherUtils.launchApp(contextArg, mirroringIntent),
241                             metaData.getAlternateLaunchCallback())));
242                 } else if (metaData.getIsMirroring()) {
243                     Intent intent = new Intent(Intent.ACTION_MAIN)
244                             .setComponent(metaData.getComponentName())
245                             .addCategory(Intent.CATEGORY_LAUNCHER)
246                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
247                     launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(),
248                             item.getDisplayName(), new AppMetaData(metaData.getDisplayName(),
249                             metaData.getComponentName(), metaData.getIcon(),
250                             metaData.getIsDistractionOptimized(), /* isMirroring= */ false,
251                             metaData.getIsDisabledByTos(),
252                                     contextArg ->
253                                             AppLauncherUtils.launchApp(contextArg, intent),
254                             metaData.getAlternateLaunchCallback())));
255                 } else {
256                     launcherListCopy.add(item);
257                 }
258             } else {
259                 launcherListCopy.add(item);
260             }
261         }
262         mCurrentAppList.postValue(launcherListCopy);
263     }
264 }
265