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