1 /* 2 * Copyright (C) 2018 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; 18 19 import android.content.ComponentName; 20 import android.content.Intent; 21 import android.util.Log; 22 23 import androidx.annotation.VisibleForTesting; 24 import androidx.lifecycle.LiveData; 25 import androidx.lifecycle.MutableLiveData; 26 import androidx.lifecycle.ViewModel; 27 28 import com.android.car.carlauncher.LauncherItemProto.LauncherItemListMessage; 29 import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.OutputStream; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.Executors; 47 import java.util.stream.Collectors; 48 49 /** 50 * A launcher model decides how the apps are displayed. 51 */ 52 public class LauncherViewModel extends ViewModel { 53 private static final String TAG = "LauncherModel"; 54 private boolean mIsCustomized; 55 private boolean mIsAlphabetized; 56 private boolean mAppOrderRead; 57 public static final String ORDER_FILE_NAME = "order.data"; 58 private Map<ComponentName, LauncherItem> mLauncherItemMap = new HashMap<>(); 59 private final MutableLiveData<List<LauncherItem>> mCurrentLauncher = 60 new MutableLiveData<>(new ArrayList<>()); 61 private List<LauncherItemMessage> mItemsFromProto = new ArrayList<>(); 62 private List<LauncherItem> mItemsFromPlatform = new ArrayList<>(); 63 private List<LauncherItem> mFinalItems = new ArrayList<>(); 64 private OutputStream mOutputStream; 65 private InputStream mInputStream; 66 private LauncherItemHelper mLauncherItemHelper; 67 private File mFileDir; 68 LauncherViewModel(File fileDir)69 public LauncherViewModel(File fileDir) { 70 mLauncherItemHelper = new LauncherItemHelper(); 71 mFileDir = fileDir; 72 } 73 74 public static final Comparator<LauncherItem> ALPHABETICAL_COMPARATOR = Comparator.comparing( 75 LauncherItem::getDisplayName, String::compareToIgnoreCase); 76 isCustomized()77 public boolean isCustomized() { 78 return mIsCustomized; 79 } 80 getCurrentLauncher()81 public LiveData<List<LauncherItem>> getCurrentLauncher() { 82 return mCurrentLauncher; 83 } 84 getLauncherItemMap()85 public Map<ComponentName, LauncherItem> getLauncherItemMap() { 86 return mLauncherItemMap; 87 } 88 89 @VisibleForTesting getOutputStream()90 OutputStream getOutputStream() { 91 return mOutputStream; 92 } 93 94 @VisibleForTesting setOutputStream(OutputStream outputStream)95 void setOutputStream(OutputStream outputStream) { 96 mOutputStream = outputStream; 97 } 98 99 @VisibleForTesting setInputStream(InputStream inputStream)100 void setInputStream(InputStream inputStream) { 101 mInputStream = inputStream; 102 } 103 104 @VisibleForTesting setLauncherItemHelper(LauncherItemHelper helper)105 void setLauncherItemHelper(LauncherItemHelper helper) { 106 mLauncherItemHelper = helper; 107 } 108 109 /** 110 * Populate the apps based on alphabetical order and create mapping from packageName to 111 * LauncherItem. Each item in the current launcher is AppItem. 112 */ generateAlphabetizedAppOrder(AppLauncherUtils.LauncherAppsInfo launcherAppsInfo)113 public void generateAlphabetizedAppOrder(AppLauncherUtils.LauncherAppsInfo launcherAppsInfo) { 114 List<LauncherItem> tempList = new ArrayList<>(); 115 mLauncherItemMap.clear(); 116 List<AppMetaData> apps = launcherAppsInfo.getLaunchableComponentsList(); 117 for (AppMetaData app : apps) { 118 LauncherItem launcherItem = new AppItem(app.getPackageName(), app.getClassName(), 119 app.getDisplayName(), app); 120 tempList.add(launcherItem); 121 mLauncherItemMap.put(app.getComponentName(), launcherItem); 122 } 123 Collections.sort(tempList, LauncherViewModel.ALPHABETICAL_COMPARATOR); 124 mItemsFromPlatform = tempList; 125 mIsAlphabetized = true; 126 createAppList(); 127 } 128 129 /** 130 * Populate the current launcher in the correct order if there are any order 131 * recorded and update the mapping 132 */ 133 updateAppsOrder()134 public void updateAppsOrder() { 135 mItemsFromProto.clear(); 136 try { 137 File order = new File(mFileDir, ORDER_FILE_NAME); 138 if (order.exists()) { 139 if (mInputStream == null) { 140 mInputStream = new FileInputStream(order); 141 } 142 LauncherItemListMessage launcherItemListMsg = 143 LauncherItemListMessage.parseDelimitedFrom(mInputStream); 144 if (launcherItemListMsg != null 145 && launcherItemListMsg.getLauncherItemMessageCount() != 0) { 146 mIsCustomized = true; 147 mItemsFromProto = mLauncherItemHelper.sortLauncherItemListMsg( 148 launcherItemListMsg); 149 } 150 } 151 } catch (IOException e) { 152 Log.e(TAG, "Read from input stream not successfully"); 153 } finally { 154 if (mInputStream != null) { 155 try { 156 mInputStream.close(); 157 mInputStream = null; 158 } catch (IOException e) { 159 Log.e(TAG, "Unable to close input stream"); 160 } 161 } 162 mAppOrderRead = true; 163 createAppList(); 164 } 165 } 166 createAppList()167 private void createAppList() { 168 Set<ComponentName> componentNames = new HashSet<>(); 169 if (mIsAlphabetized && mAppOrderRead) { 170 mFinalItems.clear(); 171 if (!mItemsFromProto.isEmpty()) { 172 for (LauncherItemMessage item : mItemsFromProto) { 173 LauncherItem itemFromMap = mLauncherItemMap.get( 174 new ComponentName(item.getPackageName(), item.getClassName())); 175 // If item exists in proto but not in map, (e.g, when app 176 // is disabled from Settings), it can be ignored 177 if (itemFromMap != null) { 178 mFinalItems.add(itemFromMap); 179 componentNames.add(new 180 ComponentName(itemFromMap.getPackageName(), 181 itemFromMap.getClassName())); 182 } 183 } 184 // If item exists in map but not in proto (e.g, when app 185 // is enabled from Settings), app must be added to the current list 186 List<ComponentName> componentNamesNotInProto = mLauncherItemMap.keySet() 187 .stream() 188 .filter(element -> !componentNames.contains(element)) 189 .collect(Collectors.toList()); 190 if (!componentNamesNotInProto.isEmpty()) { 191 Collections.sort(componentNamesNotInProto); 192 for (ComponentName componentName: componentNamesNotInProto) { 193 mFinalItems.add(mLauncherItemMap.get(componentName)); 194 } 195 } 196 mCurrentLauncher.postValue(mFinalItems); 197 } else { 198 mCurrentLauncher.postValue(mItemsFromPlatform); 199 } 200 mIsAlphabetized = false; 201 mAppOrderRead = false; 202 } 203 } 204 205 /** 206 * Update an AppItem's AppMetaData isMirroring state and its launchCallback 207 * Then, post the updated live data object 208 */ 209 // TODO (b/272796126): refactor to data model and move deep copying to inside DiffUtil updateMirroringItem(String packageName, Intent mirroringIntent)210 public void updateMirroringItem(String packageName, Intent mirroringIntent) { 211 List<LauncherItem> launcherList = mCurrentLauncher.getValue(); 212 if (launcherList == null) { 213 return; 214 } 215 List<LauncherItem> launcherListCopy = new ArrayList<>(); 216 for (LauncherItem item : launcherList) { 217 if (item instanceof AppItem) { 218 AppMetaData metaData = ((AppItem) item).getAppMetaData(); 219 if (item.getPackageName().equals(packageName)) { 220 launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(), 221 item.getDisplayName(), new AppMetaData(metaData.getDisplayName(), 222 metaData.getComponentName(), metaData.getIcon(), 223 metaData.getIsDistractionOptimized(), /* isMirroring= */ true, 224 contextArg -> AppLauncherUtils.launchApp(contextArg, mirroringIntent), 225 metaData.getAlternateLaunchCallback()))); 226 } else if (metaData.getIsMirroring()) { 227 Intent intent = new Intent(Intent.ACTION_MAIN) 228 .setComponent(metaData.getComponentName()) 229 .addCategory(Intent.CATEGORY_LAUNCHER) 230 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 231 launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(), 232 item.getDisplayName(), new AppMetaData(metaData.getDisplayName(), 233 metaData.getComponentName(), metaData.getIcon(), 234 metaData.getIsDistractionOptimized(), /* isMirroring= */ false, 235 contextArg -> AppLauncherUtils.launchApp(contextArg, intent), 236 metaData.getAlternateLaunchCallback()))); 237 } else { 238 launcherListCopy.add(item); 239 } 240 } else { 241 launcherListCopy.add(item); 242 } 243 } 244 mCurrentLauncher.postValue(launcherListCopy); 245 } 246 247 /** 248 * Record the current apps' order to a file if needed 249 */ maybeSaveAppsOrder()250 public void maybeSaveAppsOrder() { 251 if (isCustomized()) { 252 ExecutorService executorService = Executors.newSingleThreadExecutor(); 253 executorService.execute(() -> { 254 writeToFile(); 255 executorService.shutdown(); 256 }); 257 } 258 } 259 writeToFile()260 protected void writeToFile() { 261 LauncherItemListMessage launcherItemListMessage = mLauncherItemHelper.launcherList2Msg( 262 mCurrentLauncher.getValue()); 263 try { 264 if (mOutputStream == null) { 265 mOutputStream = new FileOutputStream(new File(mFileDir, ORDER_FILE_NAME), false); 266 } 267 launcherItemListMessage.writeDelimitedTo(mOutputStream); 268 } catch (IOException e) { 269 Log.e(TAG, "Order not written to file successfully"); 270 } finally { 271 try { 272 if (mOutputStream != null) { 273 mOutputStream.flush(); 274 if (mOutputStream instanceof FileOutputStream) { 275 ((FileOutputStream) mOutputStream).getFD().sync(); 276 } 277 mOutputStream.close(); 278 mOutputStream = null; 279 } 280 } catch (IOException e) { 281 Log.e(TAG, "Unable to close output stream"); 282 } 283 } 284 } 285 286 /** 287 * Move an app to a specified index 288 */ movePackage(int index, AppMetaData app)289 public void movePackage(int index, AppMetaData app) { 290 List<LauncherItem> current = mCurrentLauncher.getValue(); 291 LauncherItem item = mLauncherItemMap.get(app.getComponentName()); 292 if (current != null && current.size() != 0 && index < current.size() && item != null) { 293 current.remove(item); 294 current.add(index, item); 295 mIsCustomized = true; 296 mCurrentLauncher.postValue(current); 297 } 298 } 299 300 /** 301 * Add a new app to the current list 302 */ addPackage(AppMetaData app)303 public void addPackage(AppMetaData app) { 304 if (app != null && !mLauncherItemMap.containsKey(app.getComponentName())) { 305 List<LauncherItem> current = mCurrentLauncher.getValue(); 306 LauncherItem launcherItem = new AppItem(app.getPackageName(), app.getClassName(), 307 app.getDisplayName(), app); 308 current.add(launcherItem); 309 mLauncherItemMap.put(app.getComponentName(), launcherItem); 310 if (!mIsCustomized) { 311 Collections.sort(current, LauncherViewModel.ALPHABETICAL_COMPARATOR); 312 } 313 mCurrentLauncher.postValue(current); 314 } 315 } 316 317 /** 318 * Remove an app from the current launcher 319 */ removePackage(AppMetaData app)320 public void removePackage(AppMetaData app) { 321 if (app != null && mLauncherItemMap.containsKey(app.getComponentName())) { 322 List<LauncherItem> current = mCurrentLauncher.getValue(); 323 LauncherItem launcherItem = mLauncherItemMap.get(app.getComponentName()); 324 if (current != null && current.size() != 0) { 325 current.remove(launcherItem); 326 mCurrentLauncher.postValue(current); 327 mLauncherItemMap.remove(app.getComponentName()); 328 } 329 } 330 } 331 332 /** 333 * Check if the order file exists 334 */ doesFileExist()335 public boolean doesFileExist() { 336 File order = new File(mFileDir, ORDER_FILE_NAME); 337 return order.exists(); 338 } 339 setCustomized(boolean customized)340 public void setCustomized(boolean customized) { 341 mIsCustomized = customized; 342 } 343 setAppOrderRead(boolean appOrderRead)344 public void setAppOrderRead(boolean appOrderRead) { 345 mAppOrderRead = appOrderRead; 346 } 347 } 348