1/*
2 * Copyright (C) 2016 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 */
16package com.android.launcher3.model;
17
18import android.content.Context;
19import android.content.Intent;
20import android.content.pm.LauncherActivityInfo;
21import android.os.Process;
22import android.os.UserHandle;
23import android.util.ArrayMap;
24import android.util.LongSparseArray;
25import android.util.Pair;
26import com.android.launcher3.AllAppsList;
27import com.android.launcher3.AppInfo;
28import com.android.launcher3.FolderInfo;
29import com.android.launcher3.InvariantDeviceProfile;
30import com.android.launcher3.ItemInfo;
31import com.android.launcher3.LauncherAppState;
32import com.android.launcher3.LauncherAppWidgetInfo;
33import com.android.launcher3.LauncherModel;
34import com.android.launcher3.LauncherModel.CallbackTask;
35import com.android.launcher3.LauncherModel.Callbacks;
36import com.android.launcher3.LauncherSettings;
37import com.android.launcher3.ShortcutInfo;
38import com.android.launcher3.Utilities;
39import com.android.launcher3.util.GridOccupancy;
40import com.android.launcher3.util.ManagedProfileHeuristic.UserFolderInfo;
41import com.android.launcher3.util.Provider;
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * Task to add auto-created workspace items.
47 */
48public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
49
50    private final Provider<List<Pair<ItemInfo, Object>>> mAppsProvider;
51
52    /**
53     * @param appsProvider items to add on the workspace
54     */
55    public AddWorkspaceItemsTask(Provider<List<Pair<ItemInfo, Object>>> appsProvider) {
56        mAppsProvider = appsProvider;
57    }
58
59    @Override
60    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
61        List<Pair<ItemInfo, Object>> workspaceApps = mAppsProvider.get();
62        if (workspaceApps.isEmpty()) {
63            return;
64        }
65        Context context = app.getContext();
66
67        final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>();
68        final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<>();
69        ArrayMap<UserHandle, UserFolderInfo> userFolderMap = new ArrayMap<>();
70
71        // Get the list of workspace screens.  We need to append to this list and
72        // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
73        // called.
74        ArrayList<Long> workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context);
75        synchronized(dataModel) {
76
77            List<ItemInfo> filteredItems = new ArrayList<>();
78            for (Pair<ItemInfo, Object> entry : workspaceApps) {
79                ItemInfo item = entry.first;
80                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
81                        item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
82                    // Short-circuit this logic if the icon exists somewhere on the workspace
83                    if (shortcutExists(dataModel, item.getIntent(), item.user)) {
84                        continue;
85                    }
86                }
87
88                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
89                    if (item instanceof AppInfo) {
90                        item = ((AppInfo) item).makeShortcut();
91                    }
92
93                    if (!Process.myUserHandle().equals(item.user)) {
94                        // Check if this belongs to a work folder.
95                        if (!(entry.second instanceof LauncherActivityInfo)) {
96                            continue;
97                        }
98
99                        UserFolderInfo userFolderInfo = userFolderMap.get(item.user);
100                        if (userFolderInfo == null) {
101                            userFolderInfo = new UserFolderInfo(context, item.user, dataModel);
102                            userFolderMap.put(item.user, userFolderInfo);
103                        }
104                        item = userFolderInfo.convertToWorkspaceItem(
105                                (ShortcutInfo) item, (LauncherActivityInfo) entry.second);
106                    }
107                }
108                if (item != null) {
109                    filteredItems.add(item);
110                }
111            }
112
113            for (ItemInfo item : filteredItems) {
114                // Find appropriate space for the item.
115                Pair<Long, int[]> coords = findSpaceForItem(app, dataModel, workspaceScreens,
116                        addedWorkspaceScreensFinal, item.spanX, item.spanY);
117                long screenId = coords.first;
118                int[] cordinates = coords.second;
119
120                ItemInfo itemInfo;
121                if (item instanceof ShortcutInfo || item instanceof FolderInfo ||
122                        item instanceof LauncherAppWidgetInfo) {
123                    itemInfo = item;
124                } else if (item instanceof AppInfo) {
125                    itemInfo = ((AppInfo) item).makeShortcut();
126                } else {
127                    throw new RuntimeException("Unexpected info type");
128                }
129
130                // Add the shortcut to the db
131                getModelWriter().addItemToDatabase(itemInfo,
132                        LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId,
133                        cordinates[0], cordinates[1]);
134
135                // Save the ShortcutInfo for binding in the workspace
136                addedItemsFinal.add(itemInfo);
137            }
138        }
139
140        // Update the workspace screens
141        updateScreens(context, workspaceScreens);
142
143        if (!addedItemsFinal.isEmpty()) {
144            scheduleCallbackTask(new CallbackTask() {
145                @Override
146                public void execute(Callbacks callbacks) {
147                    final ArrayList<ItemInfo> addAnimated = new ArrayList<>();
148                    final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>();
149                    if (!addedItemsFinal.isEmpty()) {
150                        ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1);
151                        long lastScreenId = info.screenId;
152                        for (ItemInfo i : addedItemsFinal) {
153                            if (i.screenId == lastScreenId) {
154                                addAnimated.add(i);
155                            } else {
156                                addNotAnimated.add(i);
157                            }
158                        }
159                    }
160                    callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
161                            addNotAnimated, addAnimated);
162                }
163            });
164        }
165
166        for (UserFolderInfo userFolderInfo : userFolderMap.values()) {
167            userFolderInfo.applyPendingState(getModelWriter());
168        }
169    }
170
171    protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) {
172        LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens);
173    }
174
175    /**
176     * Returns true if the shortcuts already exists on the workspace. This must be called after
177     * the workspace has been loaded. We identify a shortcut by its intent.
178     */
179    protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandle user) {
180        final String compPkgName, intentWithPkg, intentWithoutPkg;
181        if (intent == null) {
182            // Skip items with null intents
183            return true;
184        }
185        if (intent.getComponent() != null) {
186            // If component is not null, an intent with null package will produce
187            // the same result and should also be a match.
188            compPkgName = intent.getComponent().getPackageName();
189            if (intent.getPackage() != null) {
190                intentWithPkg = intent.toUri(0);
191                intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
192            } else {
193                intentWithPkg = new Intent(intent).setPackage(compPkgName).toUri(0);
194                intentWithoutPkg = intent.toUri(0);
195            }
196        } else {
197            compPkgName = null;
198            intentWithPkg = intent.toUri(0);
199            intentWithoutPkg = intent.toUri(0);
200        }
201
202        boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent);
203        synchronized (dataModel) {
204            for (ItemInfo item : dataModel.itemsIdMap) {
205                if (item instanceof ShortcutInfo) {
206                    ShortcutInfo info = (ShortcutInfo) item;
207                    if (item.getIntent() != null && info.user.equals(user)) {
208                        Intent copyIntent = new Intent(item.getIntent());
209                        copyIntent.setSourceBounds(intent.getSourceBounds());
210                        String s = copyIntent.toUri(0);
211                        if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
212                            return true;
213                        }
214
215                        // checking for existing promise icon with same package name
216                        if (isLauncherAppTarget
217                                && info.isPromise()
218                                && info.hasStatusFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)
219                                && info.getTargetComponent() != null
220                                && compPkgName != null
221                                && compPkgName.equals(info.getTargetComponent().getPackageName())) {
222                            return true;
223                        }
224                    }
225                }
226            }
227        }
228        return false;
229    }
230
231    /**
232     * Find a position on the screen for the given size or adds a new screen.
233     * @return screenId and the coordinates for the item.
234     */
235    protected Pair<Long, int[]> findSpaceForItem(
236            LauncherAppState app, BgDataModel dataModel,
237            ArrayList<Long> workspaceScreens,
238            ArrayList<Long> addedWorkspaceScreensFinal,
239            int spanX, int spanY) {
240        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
241
242        // Use sBgItemsIdMap as all the items are already loaded.
243        synchronized (dataModel) {
244            for (ItemInfo info : dataModel.itemsIdMap) {
245                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
246                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
247                    if (items == null) {
248                        items = new ArrayList<>();
249                        screenItems.put(info.screenId, items);
250                    }
251                    items.add(info);
252                }
253            }
254        }
255
256        // Find appropriate space for the item.
257        long screenId = 0;
258        int[] cordinates = new int[2];
259        boolean found = false;
260
261        int screenCount = workspaceScreens.size();
262        // First check the preferred screen.
263        int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
264        if (preferredScreenIndex < screenCount) {
265            screenId = workspaceScreens.get(preferredScreenIndex);
266            found = findNextAvailableIconSpaceInScreen(
267                    app, screenItems.get(screenId), cordinates, spanX, spanY);
268        }
269
270        if (!found) {
271            // Search on any of the screens starting from the first screen.
272            for (int screen = 1; screen < screenCount; screen++) {
273                screenId = workspaceScreens.get(screen);
274                if (findNextAvailableIconSpaceInScreen(
275                        app, screenItems.get(screenId), cordinates, spanX, spanY)) {
276                    // We found a space for it
277                    found = true;
278                    break;
279                }
280            }
281        }
282
283        if (!found) {
284            // Still no position found. Add a new screen to the end.
285            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
286                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
287                    .getLong(LauncherSettings.Settings.EXTRA_VALUE);
288
289            // Save the screen id for binding in the workspace
290            workspaceScreens.add(screenId);
291            addedWorkspaceScreensFinal.add(screenId);
292
293            // If we still can't find an empty space, then God help us all!!!
294            if (!findNextAvailableIconSpaceInScreen(
295                    app, screenItems.get(screenId), cordinates, spanX, spanY)) {
296                throw new RuntimeException("Can't find space to add the item");
297            }
298        }
299        return Pair.create(screenId, cordinates);
300    }
301
302    private boolean findNextAvailableIconSpaceInScreen(
303            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
304            int[] xy, int spanX, int spanY) {
305        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
306
307        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
308        if (occupiedPos != null) {
309            for (ItemInfo r : occupiedPos) {
310                occupied.markCells(r, true);
311            }
312        }
313        return occupied.findVacantCell(xy, spanX, spanY);
314    }
315
316}
317