1 /*
2  * Copyright (C) 2015 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.launcher3.util;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.content.pm.PackageInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.os.Build;
26 import android.util.Log;
27 
28 import com.android.launcher3.FolderInfo;
29 import com.android.launcher3.ItemInfo;
30 import com.android.launcher3.LauncherAppState;
31 import com.android.launcher3.LauncherFiles;
32 import com.android.launcher3.LauncherModel;
33 import com.android.launcher3.MainThreadExecutor;
34 import com.android.launcher3.R;
35 import com.android.launcher3.ShortcutInfo;
36 import com.android.launcher3.Utilities;
37 import com.android.launcher3.compat.LauncherActivityInfoCompat;
38 import com.android.launcher3.compat.LauncherAppsCompat;
39 import com.android.launcher3.compat.UserHandleCompat;
40 import com.android.launcher3.compat.UserManagerCompat;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.Comparator;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Set;
49 
50 /**
51  * Handles addition of app shortcuts for managed profiles.
52  * Methods of class should only be called on {@link LauncherModel#sWorkerThread}.
53  */
54 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
55 public class ManagedProfileHeuristic {
56 
57     private static final String TAG = "ManagedProfileHeuristic";
58 
59     /**
60      * Maintain a set of packages installed per user.
61      */
62     private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_";
63 
64     private static final String USER_FOLDER_ID_PREFIX = "user_folder_";
65 
66     /**
67      * Duration (in milliseconds) for which app shortcuts will be added to work folder.
68      */
69     private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000;
70 
get(Context context, UserHandleCompat user)71     public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) {
72         if (Utilities.ATLEAST_LOLLIPOP && !UserHandleCompat.myUserHandle().equals(user)) {
73             return new ManagedProfileHeuristic(context, user);
74         }
75         return null;
76     }
77 
78     private final Context mContext;
79     private final UserHandleCompat mUser;
80     private final LauncherModel mModel;
81 
82     private final SharedPreferences mPrefs;
83     private final long mUserSerial;
84     private final long mUserCreationTime;
85     private final String mPackageSetKey;
86 
87     private ArrayList<ShortcutInfo> mHomescreenApps;
88     private ArrayList<ShortcutInfo> mWorkFolderApps;
89     private HashMap<ShortcutInfo, Long> mShortcutToInstallTimeMap;
90 
ManagedProfileHeuristic(Context context, UserHandleCompat user)91     private ManagedProfileHeuristic(Context context, UserHandleCompat user) {
92         mContext = context;
93         mUser = user;
94         mModel = LauncherAppState.getInstance().getModel();
95 
96         UserManagerCompat userManager = UserManagerCompat.getInstance(context);
97         mUserSerial = userManager.getSerialNumberForUser(user);
98         mUserCreationTime = userManager.getUserCreationTime(user);
99         mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial;
100 
101         mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY,
102                 Context.MODE_PRIVATE);
103     }
104 
initVars()105     private void initVars() {
106         mHomescreenApps = new ArrayList<>();
107         mWorkFolderApps = new ArrayList<>();
108         mShortcutToInstallTimeMap = new HashMap<>();
109     }
110 
111     /**
112      * Checks the list of user apps and adds icons for newly installed apps on the homescreen or
113      * workfolder.
114      */
processUserApps(List<LauncherActivityInfoCompat> apps)115     public void processUserApps(List<LauncherActivityInfoCompat> apps) {
116         initVars();
117 
118         HashSet<String> packageSet = new HashSet<>();
119         final boolean userAppsExisted = getUserApps(packageSet);
120 
121         boolean newPackageAdded = false;
122         for (LauncherActivityInfoCompat info : apps) {
123             String packageName = info.getComponentName().getPackageName();
124             if (!packageSet.contains(packageName)) {
125                 packageSet.add(packageName);
126                 newPackageAdded = true;
127                 markForAddition(info, info.getFirstInstallTime());
128             }
129         }
130 
131         if (newPackageAdded) {
132             mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
133             // Do not add shortcuts on the homescreen for the first time. This prevents the launcher
134             // getting filled with the managed user apps, when it start with a fresh DB (or after
135             // a very long time).
136             finalizeAdditions(userAppsExisted);
137         }
138     }
139 
markForAddition(LauncherActivityInfoCompat info, long installTime)140     private void markForAddition(LauncherActivityInfoCompat info, long installTime) {
141         ArrayList<ShortcutInfo> targetList =
142                 (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ?
143                         mWorkFolderApps : mHomescreenApps;
144         ShortcutInfo si = ShortcutInfo.fromActivityInfo(info, mContext);
145         mShortcutToInstallTimeMap.put(si, installTime);
146         targetList.add(si);
147     }
148 
sortList(ArrayList<ShortcutInfo> infos)149     private void sortList(ArrayList<ShortcutInfo> infos) {
150         Collections.sort(infos, new Comparator<ShortcutInfo>() {
151 
152             @Override
153             public int compare(ShortcutInfo lhs, ShortcutInfo rhs) {
154                 Long lhsTime = mShortcutToInstallTimeMap.get(lhs);
155                 Long rhsTime = mShortcutToInstallTimeMap.get(rhs);
156                 return Utilities.longCompare(lhsTime == null ? 0 : lhsTime,
157                         rhsTime == null ? 0 : rhsTime);
158             }
159         });
160     }
161 
162     /**
163      * Adds and binds shortcuts marked to be added to the work folder.
164      */
finalizeWorkFolder()165     private void finalizeWorkFolder() {
166         if (mWorkFolderApps.isEmpty()) {
167             return;
168         }
169         sortList(mWorkFolderApps);
170 
171         // Try to get a work folder.
172         String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial;
173         if (mPrefs.contains(folderIdKey)) {
174             long folderId = mPrefs.getLong(folderIdKey, 0);
175             final FolderInfo workFolder = mModel.findFolderById(folderId);
176 
177             if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) {
178                 // Could not get a work folder. Add all the icons to homescreen.
179                 mHomescreenApps.addAll(mWorkFolderApps);
180                 return;
181             }
182             saveWorkFolderShortcuts(folderId, workFolder.contents.size());
183 
184             final ArrayList<ShortcutInfo> shortcuts = mWorkFolderApps;
185             // FolderInfo could already be bound. We need to add shortcuts on the UI thread.
186             new MainThreadExecutor().execute(new Runnable() {
187 
188                 @Override
189                 public void run() {
190                     for (ShortcutInfo info : shortcuts) {
191                         workFolder.add(info);
192                     }
193                 }
194             });
195         } else {
196             // Create a new folder.
197             final FolderInfo workFolder = new FolderInfo();
198             workFolder.title = mContext.getText(R.string.work_folder_name);
199             workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null);
200 
201             // Add all shortcuts before adding it to the UI, as an empty folder might get deleted.
202             for (ShortcutInfo info : mWorkFolderApps) {
203                 workFolder.add(info);
204             }
205 
206             // Add the item to home screen and DB. This also generates an item id synchronously.
207             ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1);
208             itemList.add(workFolder);
209             mModel.addAndBindAddedWorkspaceItems(mContext, itemList);
210             mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply();
211 
212             saveWorkFolderShortcuts(workFolder.id, 0);
213         }
214     }
215 
216     /**
217      * Add work folder shortcuts to the DB.
218      */
saveWorkFolderShortcuts(long workFolderId, int startingRank)219     private void saveWorkFolderShortcuts(long workFolderId, int startingRank) {
220         for (ItemInfo info : mWorkFolderApps) {
221             info.rank = startingRank++;
222             LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0);
223         }
224     }
225 
226     /**
227      * Adds and binds all shortcuts marked for addition.
228      */
finalizeAdditions(boolean addHomeScreenShortcuts)229     private void finalizeAdditions(boolean addHomeScreenShortcuts) {
230         finalizeWorkFolder();
231 
232         if (addHomeScreenShortcuts && !mHomescreenApps.isEmpty()) {
233             sortList(mHomescreenApps);
234             mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps);
235         }
236     }
237 
238     /**
239      * Updates the list of installed apps and adds any new icons on homescreen or work folder.
240      */
processPackageAdd(String[] packages)241     public void processPackageAdd(String[] packages) {
242         initVars();
243         HashSet<String> packageSet = new HashSet<>();
244         final boolean userAppsExisted = getUserApps(packageSet);
245 
246         boolean newPackageAdded = false;
247         long installTime = System.currentTimeMillis();
248         LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext);
249 
250         for (String packageName : packages) {
251             if (!packageSet.contains(packageName)) {
252                 packageSet.add(packageName);
253                 newPackageAdded = true;
254 
255                 List<LauncherActivityInfoCompat> activities =
256                         launcherApps.getActivityList(packageName, mUser);
257                 if (!activities.isEmpty()) {
258                     markForAddition(activities.get(0), installTime);
259                 }
260             }
261         }
262 
263         if (newPackageAdded) {
264             mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
265             finalizeAdditions(userAppsExisted);
266         }
267     }
268 
269     /**
270      * Updates the list of installed packages for the user.
271      */
processPackageRemoved(String[] packages)272     public void processPackageRemoved(String[] packages) {
273         HashSet<String> packageSet = new HashSet<String>();
274         getUserApps(packageSet);
275         boolean packageRemoved = false;
276 
277         for (String packageName : packages) {
278             if (packageSet.remove(packageName)) {
279                 packageRemoved = true;
280             }
281         }
282 
283         if (packageRemoved) {
284             mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
285         }
286     }
287 
288     /**
289      * Reads the list of user apps which have already been processed.
290      * @return false if the list didn't exist, true otherwise
291      */
getUserApps(HashSet<String> outExistingApps)292     private boolean getUserApps(HashSet<String> outExistingApps) {
293         Set<String> userApps = mPrefs.getStringSet(mPackageSetKey, null);
294         if (userApps == null) {
295             return false;
296         } else {
297             outExistingApps.addAll(userApps);
298             return true;
299         }
300     }
301 
302     /**
303      * Verifies that entries corresponding to {@param users} exist and removes all invalid entries.
304      */
processAllUsers(List<UserHandleCompat> users, Context context)305     public static void processAllUsers(List<UserHandleCompat> users, Context context) {
306         if (!Utilities.ATLEAST_LOLLIPOP) {
307             return;
308         }
309         UserManagerCompat userManager = UserManagerCompat.getInstance(context);
310         HashSet<String> validKeys = new HashSet<String>();
311         for (UserHandleCompat user : users) {
312             addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys);
313         }
314 
315         SharedPreferences prefs = context.getSharedPreferences(
316                 LauncherFiles.MANAGED_USER_PREFERENCES_KEY,
317                 Context.MODE_PRIVATE);
318         SharedPreferences.Editor editor = prefs.edit();
319         for (String key : prefs.getAll().keySet()) {
320             if (!validKeys.contains(key)) {
321                 editor.remove(key);
322             }
323         }
324         editor.apply();
325     }
326 
addAllUserKeys(long userSerial, HashSet<String> keysOut)327     private static void addAllUserKeys(long userSerial, HashSet<String> keysOut) {
328         keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial);
329         keysOut.add(USER_FOLDER_ID_PREFIX + userSerial);
330     }
331 
332     /**
333      * For each user, if a work folder has not been created, mark it such that the folder will
334      * never get created.
335      */
markExistingUsersForNoFolderCreation(Context context)336     public static void markExistingUsersForNoFolderCreation(Context context) {
337         UserManagerCompat userManager = UserManagerCompat.getInstance(context);
338         UserHandleCompat myUser = UserHandleCompat.myUserHandle();
339 
340         SharedPreferences prefs = null;
341         for (UserHandleCompat user : userManager.getUserProfiles()) {
342             if (myUser.equals(user)) {
343                 continue;
344             }
345 
346             if (prefs == null) {
347                 prefs = context.getSharedPreferences(
348                         LauncherFiles.MANAGED_USER_PREFERENCES_KEY,
349                         Context.MODE_PRIVATE);
350             }
351             String folderIdKey = USER_FOLDER_ID_PREFIX + userManager.getSerialNumberForUser(user);
352             if (!prefs.contains(folderIdKey)) {
353                 prefs.edit().putLong(folderIdKey, ItemInfo.NO_ID).apply();
354             }
355         }
356     }
357 }
358