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