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.content.pm.PackageManager;
26 import android.database.Cursor;
27 import android.database.CursorWrapper;
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.WorkspaceItemInfo;
37 import com.android.launcher3.icons.IconCache;
38 import com.android.launcher3.InvariantDeviceProfile;
39 import com.android.launcher3.ItemInfo;
40 import com.android.launcher3.LauncherAppState;
41 import com.android.launcher3.LauncherSettings;
42 import com.android.launcher3.Utilities;
43 import com.android.launcher3.Workspace;
44 import com.android.launcher3.compat.LauncherAppsCompat;
45 import com.android.launcher3.config.FeatureFlags;
46 import com.android.launcher3.icons.BitmapInfo;
47 import com.android.launcher3.icons.LauncherIcons;
48 import com.android.launcher3.logging.FileLog;
49 import com.android.launcher3.util.ContentWriter;
50 import com.android.launcher3.util.GridOccupancy;
51 import com.android.launcher3.util.IntArray;
52 import com.android.launcher3.util.IntSparseArrayMap;
53 
54 import java.net.URISyntaxException;
55 import java.security.InvalidParameterException;
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 PackageManager mPM;
68     private final IconCache mIconCache;
69     private final InvariantDeviceProfile mIDP;
70 
71     private final IntArray itemsToRemove = new IntArray();
72     private final IntArray restoredRows = new IntArray();
73     private final IntSparseArrayMap<GridOccupancy> occupied = new IntSparseArrayMap<>();
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 int id;
94     public int 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         mPM = mContext.getPackageManager();
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 = getInt(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 
loadSimpleWorkspaceItem()148     public WorkspaceItemInfo loadSimpleWorkspaceItem() {
149         final WorkspaceItemInfo info = new WorkspaceItemInfo();
150         // Non-app shortcuts are only supported for current user.
151         info.user = user;
152         info.itemType = itemType;
153         info.title = getTitle();
154         // the fallback icon
155         if (!loadIcon(info)) {
156             info.applyFrom(mIconCache.getDefaultIcon(info.user));
157         }
158 
159         // TODO: If there's an explicit component and we can't install that, delete it.
160 
161         return info;
162     }
163 
164     /**
165      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
166      */
loadIcon(WorkspaceItemInfo info)167     protected boolean loadIcon(WorkspaceItemInfo info) {
168         try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
169             return loadIcon(info, li);
170         }
171     }
172 
173     /**
174      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
175      */
loadIcon(WorkspaceItemInfo info, LauncherIcons li)176     protected boolean loadIcon(WorkspaceItemInfo info, LauncherIcons li) {
177         if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
178             String packageName = getString(iconPackageIndex);
179             String resourceName = getString(iconResourceIndex);
180             if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
181                 info.iconResource = new ShortcutIconResource();
182                 info.iconResource.packageName = packageName;
183                 info.iconResource.resourceName = resourceName;
184                 BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
185                 if (iconInfo != null) {
186                     info.applyFrom(iconInfo);
187                     return true;
188                 }
189             }
190         }
191 
192         // Failed to load from resource, try loading from DB.
193         byte[] data = getBlob(iconIndex);
194         try {
195             info.applyFrom(li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)));
196             return true;
197         } catch (Exception e) {
198             Log.e(TAG, "Failed to decode byte array for info " + info, e);
199             return false;
200         }
201     }
202 
203     /**
204      * Returns the title or empty string
205      */
getTitle()206     private String getTitle() {
207         String title = getString(titleIndex);
208         return TextUtils.isEmpty(title) ? "" : Utilities.trim(title);
209     }
210 
211     /**
212      * Make an WorkspaceItemInfo object for a restored application or shortcut item that points
213      * to a package that is not yet installed on the system.
214      */
getRestoredItemInfo(Intent intent)215     public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
216         final WorkspaceItemInfo info = new WorkspaceItemInfo();
217         info.user = user;
218         info.intent = intent;
219 
220         // the fallback icon
221         if (!loadIcon(info)) {
222             mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
223         }
224 
225         if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
226             String title = getTitle();
227             if (!TextUtils.isEmpty(title)) {
228                 info.title = Utilities.trim(title);
229             }
230         } else if (hasRestoreFlag(WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON)) {
231             if (TextUtils.isEmpty(info.title)) {
232                 info.title = getTitle();
233             }
234         } else {
235             throw new InvalidParameterException("Invalid restoreType " + restoreFlag);
236         }
237 
238         info.contentDescription = mPM.getUserBadgedLabel(info.title, info.user);
239         info.itemType = itemType;
240         info.status = restoreFlag;
241         return info;
242     }
243 
244     /**
245      * Make an WorkspaceItemInfo object for a shortcut that is an application.
246      */
getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)247     public WorkspaceItemInfo getAppShortcutInfo(
248             Intent intent, boolean allowMissingTarget, boolean useLowResIcon) {
249         if (user == null) {
250             Log.d(TAG, "Null user found in getShortcutInfo");
251             return null;
252         }
253 
254         ComponentName componentName = intent.getComponent();
255         if (componentName == null) {
256             Log.d(TAG, "Missing component found in getShortcutInfo");
257             return null;
258         }
259 
260         Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
261         newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
262         newIntent.setComponent(componentName);
263         LauncherActivityInfo lai = LauncherAppsCompat.getInstance(mContext)
264                 .resolveActivity(newIntent, user);
265         if ((lai == null) && !allowMissingTarget) {
266             Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
267             return null;
268         }
269 
270         final WorkspaceItemInfo info = new WorkspaceItemInfo();
271         info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
272         info.user = user;
273         info.intent = newIntent;
274 
275         mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
276         if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
277             loadIcon(info);
278         }
279 
280         if (lai != null) {
281             AppInfo.updateRuntimeFlagsForActivityTarget(info, lai);
282         }
283 
284         // from the db
285         if (TextUtils.isEmpty(info.title)) {
286             info.title = getTitle();
287         }
288 
289         // fall back to the class name of the activity
290         if (info.title == null) {
291             info.title = componentName.getClassName();
292         }
293 
294         info.contentDescription = mPM.getUserBadgedLabel(info.title, info.user);
295         return info;
296     }
297 
298     /**
299      * Returns a {@link ContentWriter} which can be used to update the current item.
300      */
updater()301     public ContentWriter updater() {
302        return new ContentWriter(mContext, new ContentWriter.CommitParams(
303                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
304     }
305 
306     /**
307      * Marks the current item for removal
308      */
markDeleted(String reason)309     public void markDeleted(String reason) {
310         FileLog.e(TAG, reason);
311         itemsToRemove.add(id);
312     }
313 
314     /**
315      * Removes any items marked for removal.
316      * @return true is any item was removed.
317      */
commitDeleted()318     public boolean commitDeleted() {
319         if (itemsToRemove.size() > 0) {
320             // Remove dead items
321             mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
322                     Utilities.createDbSelectionQuery(
323                             LauncherSettings.Favorites._ID, itemsToRemove), null);
324             return true;
325         }
326         return false;
327     }
328 
329     /**
330      * Marks the current item as restored
331      */
markRestored()332     public void markRestored() {
333         if (restoreFlag != 0) {
334             restoredRows.add(id);
335             restoreFlag = 0;
336         }
337     }
338 
hasRestoreFlag(int flagMask)339     public boolean hasRestoreFlag(int flagMask) {
340         return (restoreFlag & flagMask) != 0;
341     }
342 
commitRestoredItems()343     public void commitRestoredItems() {
344         if (restoredRows.size() > 0) {
345             // Update restored items that no longer require special handling
346             ContentValues values = new ContentValues();
347             values.put(LauncherSettings.Favorites.RESTORED, 0);
348             mContext.getContentResolver().update(LauncherSettings.Favorites.CONTENT_URI, values,
349                     Utilities.createDbSelectionQuery(
350                             LauncherSettings.Favorites._ID, restoredRows), null);
351         }
352     }
353 
354     /**
355      * Returns true is the item is on workspace or hotseat
356      */
isOnWorkspaceOrHotseat()357     public boolean isOnWorkspaceOrHotseat() {
358         return container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
359                 container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
360     }
361 
362     /**
363      * Applies the following properties:
364      * {@link ItemInfo#id}
365      * {@link ItemInfo#container}
366      * {@link ItemInfo#screenId}
367      * {@link ItemInfo#cellX}
368      * {@link ItemInfo#cellY}
369      */
applyCommonProperties(ItemInfo info)370     public void applyCommonProperties(ItemInfo info) {
371         info.id = id;
372         info.container = container;
373         info.screenId = getInt(screenIndex);
374         info.cellX = getInt(cellXIndex);
375         info.cellY = getInt(cellYIndex);
376     }
377 
378     /**
379      * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
380      * otherwise marks it for deletion.
381      */
checkAndAddItem(ItemInfo info, BgDataModel dataModel)382     public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
383         if (checkItemPlacement(info)) {
384             dataModel.addItem(mContext, info, false);
385         } else {
386             markDeleted("Item position overlap");
387         }
388     }
389 
390     /**
391      * check & update map of what's occupied; used to discard overlapping/invalid items
392      */
checkItemPlacement(ItemInfo item)393     protected boolean checkItemPlacement(ItemInfo item) {
394         int containerIndex = item.screenId;
395         if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
396             final GridOccupancy hotseatOccupancy =
397                     occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT);
398 
399             if (item.screenId >= mIDP.numHotseatIcons) {
400                 Log.e(TAG, "Error loading shortcut " + item
401                         + " into hotseat position " + item.screenId
402                         + ", position out of bounds: (0 to " + (mIDP.numHotseatIcons - 1)
403                         + ")");
404                 return false;
405             }
406 
407             if (hotseatOccupancy != null) {
408                 if (hotseatOccupancy.cells[(int) item.screenId][0]) {
409                     Log.e(TAG, "Error loading shortcut into hotseat " + item
410                             + " into position (" + item.screenId + ":" + item.cellX + ","
411                             + item.cellY + ") already occupied");
412                     return false;
413                 } else {
414                     hotseatOccupancy.cells[item.screenId][0] = true;
415                     return true;
416                 }
417             } else {
418                 final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1);
419                 occupancy.cells[item.screenId][0] = true;
420                 occupied.put(LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
421                 return true;
422             }
423         } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
424             // Skip further checking if it is not the hotseat or workspace container
425             return true;
426         }
427 
428         final int countX = mIDP.numColumns;
429         final int countY = mIDP.numRows;
430         if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
431                 item.cellX < 0 || item.cellY < 0 ||
432                 item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
433             Log.e(TAG, "Error loading shortcut " + item
434                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
435                     + item.cellX + "," + item.cellY
436                     + ") out of screen bounds ( " + countX + "x" + countY + ")");
437             return false;
438         }
439 
440         if (!occupied.containsKey(item.screenId)) {
441             GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
442             if (item.screenId == Workspace.FIRST_SCREEN_ID) {
443                 // Mark the first row as occupied (if the feature is enabled)
444                 // in order to account for the QSB.
445                 screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN);
446             }
447             occupied.put(item.screenId, screen);
448         }
449         final GridOccupancy occupancy = occupied.get(item.screenId);
450 
451         // Check if any workspace icons overlap with each other
452         if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
453             occupancy.markCells(item, true);
454             return true;
455         } else {
456             Log.e(TAG, "Error loading shortcut " + item
457                     + " into cell (" + containerIndex + "-" + item.screenId + ":"
458                     + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
459                     + ") already occupied");
460             return false;
461         }
462     }
463 }
464