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