• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.model;
18 
19 import android.content.ComponentName;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.Intent.ShortcutIconResource;
24 import android.content.pm.LauncherActivityInfo;
25 import android.database.Cursor;
26 import android.database.CursorWrapper;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapFactory;
29 import android.os.UserHandle;
30 import android.provider.BaseColumns;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.LongSparseArray;
34 
35 import com.android.launcher3.AppInfo;
36 import com.android.launcher3.IconCache;
37 import com.android.launcher3.InvariantDeviceProfile;
38 import com.android.launcher3.ItemInfo;
39 import com.android.launcher3.LauncherAppState;
40 import com.android.launcher3.LauncherSettings;
41 import com.android.launcher3.ShortcutInfo;
42 import com.android.launcher3.Utilities;
43 import com.android.launcher3.Workspace;
44 import com.android.launcher3.compat.LauncherAppsCompat;
45 import com.android.launcher3.compat.UserManagerCompat;
46 import com.android.launcher3.config.FeatureFlags;
47 import com.android.launcher3.graphics.BitmapInfo;
48 import com.android.launcher3.graphics.LauncherIcons;
49 import com.android.launcher3.logging.FileLog;
50 import com.android.launcher3.util.ContentWriter;
51 import com.android.launcher3.util.GridOccupancy;
52 import com.android.launcher3.util.LongArrayMap;
53 
54 import java.net.URISyntaxException;
55 import java.security.InvalidParameterException;
56 import java.util.ArrayList;
57 
58 /**
59  * Extension of {@link Cursor} with utility methods for workspace loading.
60  */
61 public class LoaderCursor extends CursorWrapper {
62 
63     private static final String TAG = "LoaderCursor";
64 
65     public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
66 
67     private final Context mContext;
68     private final UserManagerCompat mUserManager;
69     private final IconCache mIconCache;
70     private final InvariantDeviceProfile mIDP;
71 
72     private final ArrayList<Long> itemsToRemove = new ArrayList<>();
73     private final ArrayList<Long> restoredRows = new ArrayList<>();
74     private final LongArrayMap<GridOccupancy> occupied = new LongArrayMap<>();
75 
76     private final int iconPackageIndex;
77     private final int iconResourceIndex;
78     private final int iconIndex;
79     public final int titleIndex;
80 
81     private final int idIndex;
82     private final int containerIndex;
83     private final int itemTypeIndex;
84     private final int screenIndex;
85     private final int cellXIndex;
86     private final int cellYIndex;
87     private final int profileIdIndex;
88     private final int restoredIndex;
89     private final int intentIndex;
90 
91     // Properties loaded per iteration
92     public long serialNumber;
93     public UserHandle user;
94     public long id;
95     public long container;
96     public int itemType;
97     public int restoreFlag;
98 
LoaderCursor(Cursor c, LauncherAppState app)99     public LoaderCursor(Cursor c, LauncherAppState app) {
100         super(c);
101         mContext = app.getContext();
102         mIconCache = app.getIconCache();
103         mIDP = app.getInvariantDeviceProfile();
104         mUserManager = UserManagerCompat.getInstance(mContext);
105 
106         // Init column indices
107         iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
108         iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
109         iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
110         titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
111 
112         idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
113         containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
114         itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
115         screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
116         cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
117         cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
118         profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID);
119         restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED);
120         intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
121     }
122 
123     @Override
moveToNext()124     public boolean moveToNext() {
125         boolean result = super.moveToNext();
126         if (result) {
127             // Load common properties.
128             itemType = getInt(itemTypeIndex);
129             container = getInt(containerIndex);
130             id = getLong(idIndex);
131             serialNumber = getInt(profileIdIndex);
132             user = allUsers.get(serialNumber);
133             restoreFlag = getInt(restoredIndex);
134         }
135         return result;
136     }
137 
parseIntent()138     public Intent parseIntent() {
139         String intentDescription = getString(intentIndex);
140         try {
141             return TextUtils.isEmpty(intentDescription) ?
142                     null : Intent.parseUri(intentDescription, 0);
143         } catch (URISyntaxException e) {
144             Log.e(TAG, "Error parsing Intent");
145             return null;
146         }
147     }
148 
loadSimpleShortcut()149     public ShortcutInfo loadSimpleShortcut() {
150         final ShortcutInfo info = new ShortcutInfo();
151         // Non-app shortcuts are only supported for current user.
152         info.user = user;
153         info.itemType = itemType;
154         info.title = getTitle();
155         // the fallback icon
156         if (!loadIcon(info)) {
157             mIconCache.getDefaultIcon(info.user).applyTo(info);
158         }
159 
160         // TODO: If there's an explicit component and we can't install that, delete it.
161 
162         return info;
163     }
164 
165     /**
166      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
167      */
loadIcon(ShortcutInfo info)168     protected boolean loadIcon(ShortcutInfo info) {
169         if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
170             String packageName = getString(iconPackageIndex);
171             String resourceName = getString(iconResourceIndex);
172             if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
173                 info.iconResource = new ShortcutIconResource();
174                 info.iconResource.packageName = packageName;
175                 info.iconResource.resourceName = resourceName;
176                 LauncherIcons li = LauncherIcons.obtain(mContext);
177                 BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
178                 li.recycle();
179                 if (iconInfo != null) {
180                     iconInfo.applyTo(info);
181                     return true;
182                 }
183             }
184         }
185 
186         // Failed to load from resource, try loading from DB.
187         byte[] data = getBlob(iconIndex);
188         try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
189             li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)).applyTo(info);
190             return true;
191         } catch (Exception e) {
192             Log.e(TAG, "Failed to load icon for info " + info, e);
193             return false;
194         }
195     }
196 
197     /**
198      * Returns the title or empty string
199      */
getTitle()200     private String getTitle() {
201         String title = getString(titleIndex);
202         return TextUtils.isEmpty(title) ? "" : Utilities.trim(title);
203     }
204 
205     /**
206      * Make an ShortcutInfo object for a restored application or shortcut item that points
207      * to a package that is not yet installed on the system.
208      */
getRestoredItemInfo(Intent intent)209     public ShortcutInfo getRestoredItemInfo(Intent intent) {
210         final ShortcutInfo info = new ShortcutInfo();
211         info.user = user;
212         info.intent = intent;
213 
214         // the fallback icon
215         if (!loadIcon(info)) {
216             mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
217         }
218 
219         if (hasRestoreFlag(ShortcutInfo.FLAG_RESTORED_ICON)) {
220             String title = getTitle();
221             if (!TextUtils.isEmpty(title)) {
222                 info.title = Utilities.trim(title);
223             }
224         } else if  (hasRestoreFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)) {
225             if (TextUtils.isEmpty(info.title)) {
226                 info.title = getTitle();
227             }
228         } else {
229             throw new InvalidParameterException("Invalid restoreType " + restoreFlag);
230         }
231 
232         info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
233         info.itemType = itemType;
234         info.status = restoreFlag;
235         return info;
236     }
237 
238     /**
239      * Make an ShortcutInfo object for a shortcut that is an application.
240      */
getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)241     public ShortcutInfo getAppShortcutInfo(
242             Intent intent, boolean allowMissingTarget, boolean useLowResIcon) {
243         if (user == null) {
244             Log.d(TAG, "Null user found in getShortcutInfo");
245             return null;
246         }
247 
248         ComponentName componentName = intent.getComponent();
249         if (componentName == null) {
250             Log.d(TAG, "Missing component found in getShortcutInfo");
251             return null;
252         }
253 
254         Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
255         newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
256         newIntent.setComponent(componentName);
257         LauncherActivityInfo lai = LauncherAppsCompat.getInstance(mContext)
258                 .resolveActivity(newIntent, user);
259         if ((lai == null) && !allowMissingTarget) {
260             Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
261             return null;
262         }
263 
264         final ShortcutInfo info = new ShortcutInfo();
265         info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
266         info.user = user;
267         info.intent = newIntent;
268 
269         mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
270         if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
271             loadIcon(info);
272         }
273 
274         if (lai != null) {
275             AppInfo.updateRuntimeFlagsForActivityTarget(info, lai);
276         }
277 
278         // from the db
279         if (TextUtils.isEmpty(info.title)) {
280             info.title = getTitle();
281         }
282 
283         // fall back to the class name of the activity
284         if (info.title == null) {
285             info.title = componentName.getClassName();
286         }
287 
288         info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
289         return info;
290     }
291 
292     /**
293      * Returns a {@link ContentWriter} which can be used to update the current item.
294      */
updater()295     public ContentWriter updater() {
296        return new ContentWriter(mContext, new ContentWriter.CommitParams(
297                BaseColumns._ID + "= ?", new String[]{Long.toString(id)}));
298     }
299 
300     /**
301      * Marks the current item for removal
302      */
markDeleted(String reason)303     public void markDeleted(String reason) {
304         FileLog.e(TAG, reason);
305         itemsToRemove.add(id);
306     }
307 
308     /**
309      * Removes any items marked for removal.
310      * @return true is any item was removed.
311      */
commitDeleted()312     public boolean commitDeleted() {
313         if (itemsToRemove.size() > 0) {
314             // Remove dead items
315             mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
316                     Utilities.createDbSelectionQuery(
317                             LauncherSettings.Favorites._ID, itemsToRemove), null);
318             return true;
319         }
320         return false;
321     }
322 
323     /**
324      * Marks the current item as restored
325      */
markRestored()326     public void markRestored() {
327         if (restoreFlag != 0) {
328             restoredRows.add(id);
329             restoreFlag = 0;
330         }
331     }
332 
hasRestoreFlag(int flagMask)333     public boolean hasRestoreFlag(int flagMask) {
334         return (restoreFlag & flagMask) != 0;
335     }
336 
commitRestoredItems()337     public void commitRestoredItems() {
338         if (restoredRows.size() > 0) {
339             // Update restored items that no longer require special handling
340             ContentValues values = new ContentValues();
341             values.put(LauncherSettings.Favorites.RESTORED, 0);
342             mContext.getContentResolver().update(LauncherSettings.Favorites.CONTENT_URI, values,
343                     Utilities.createDbSelectionQuery(
344                             LauncherSettings.Favorites._ID, restoredRows), null);
345         }
346     }
347 
348     /**
349      * Returns true is the item is on workspace or hotseat
350      */
isOnWorkspaceOrHotseat()351     public boolean isOnWorkspaceOrHotseat() {
352         return container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
353                 container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
354     }
355 
356     /**
357      * Applies the following properties:
358      * {@link ItemInfo#id}
359      * {@link ItemInfo#container}
360      * {@link ItemInfo#screenId}
361      * {@link ItemInfo#cellX}
362      * {@link ItemInfo#cellY}
363      */
applyCommonProperties(ItemInfo info)364     public void applyCommonProperties(ItemInfo info) {
365         info.id = id;
366         info.container = container;
367         info.screenId = getInt(screenIndex);
368         info.cellX = getInt(cellXIndex);
369         info.cellY = getInt(cellYIndex);
370     }
371 
372     /**
373      * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
374      * otherwise marks it for deletion.
375      */
checkAndAddItem(ItemInfo info, BgDataModel dataModel)376     public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
377         if (checkItemPlacement(info, dataModel.workspaceScreens)) {
378             dataModel.addItem(mContext, info, false);
379         } else {
380             markDeleted("Item position overlap");
381         }
382     }
383 
384     /**
385      * check & update map of what's occupied; used to discard overlapping/invalid items
386      */
checkItemPlacement(ItemInfo item, ArrayList<Long> workspaceScreens)387     protected boolean checkItemPlacement(ItemInfo item, ArrayList<Long> workspaceScreens) {
388         long containerIndex = item.screenId;
389         if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
390             // Return early if we detect that an item is under the hotseat button
391             if (!FeatureFlags.NO_ALL_APPS_ICON &&
392                     mIDP.isAllAppsButtonRank((int) item.screenId)) {
393                 Log.e(TAG, "Error loading shortcut into hotseat " + item
394                         + " into position (" + item.screenId + ":" + item.cellX + ","
395                         + item.cellY + ") occupied by all apps");
396                 return false;
397             }
398 
399             final GridOccupancy hotseatOccupancy =
400                     occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
401 
402             if (item.screenId >= mIDP.numHotseatIcons) {
403                 Log.e(TAG, "Error loading shortcut " + item
404                         + " into hotseat position " + item.screenId
405                         + ", position out of bounds: (0 to " + (mIDP.numHotseatIcons - 1)
406                         + ")");
407                 return false;
408             }
409 
410             if (hotseatOccupancy != null) {
411                 if (hotseatOccupancy.cells[(int) item.screenId][0]) {
412                     Log.e(TAG, "Error loading shortcut into hotseat " + item
413                             + " into position (" + item.screenId + ":" + item.cellX + ","
414                             + item.cellY + ") already occupied");
415                     return false;
416                 } else {
417                     hotseatOccupancy.cells[(int) item.screenId][0] = true;
418                     return true;
419                 }
420             } else {
421                 final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1);
422                 occupancy.cells[(int) item.screenId][0] = true;
423                 occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
424                 return true;
425             }
426         } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
427             if (!workspaceScreens.contains((Long) item.screenId)) {
428                 // The item has an invalid screen id.
429                 return false;
430             }
431         } else {
432             // Skip further checking if it is not the hotseat or workspace container
433             return true;
434         }
435 
436         final int countX = mIDP.numColumns;
437         final int countY = mIDP.numRows;
438         if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
439                 item.cellX < 0 || item.cellY < 0 ||
440                 item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
441             Log.e(TAG, "Error loading shortcut " + item
442                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
443                     + item.cellX + "," + item.cellY
444                     + ") out of screen bounds ( " + countX + "x" + countY + ")");
445             return false;
446         }
447 
448         if (!occupied.containsKey(item.screenId)) {
449             GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
450             if (item.screenId == Workspace.FIRST_SCREEN_ID) {
451                 // Mark the first row as occupied (if the feature is enabled)
452                 // in order to account for the QSB.
453                 screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN);
454             }
455             occupied.put(item.screenId, screen);
456         }
457         final GridOccupancy occupancy = occupied.get(item.screenId);
458 
459         // Check if any workspace icons overlap with each other
460         if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
461             occupancy.markCells(item, true);
462             return true;
463         } else {
464             Log.e(TAG, "Error loading shortcut " + item
465                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
466                     + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
467                     + ") already occupied");
468             return false;
469         }
470     }
471 }
472