diff options
Diffstat (limited to 'src/com/android/launcher3/model')
6 files changed, 1268 insertions, 0 deletions
diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java new file mode 100644 index 000000000..bd28560f3 --- /dev/null +++ b/src/com/android/launcher3/model/AbstractUserComparator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model; + +import android.content.Context; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Comparator; + +/** + * A comparator to arrange items based on user profiles. + */ +public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> { + + private final UserManagerCompat mUserManager; + private final UserHandleCompat mMyUser; + + public AbstractUserComparator(Context context) { + mUserManager = UserManagerCompat.getInstance(context); + mMyUser = UserHandleCompat.myUserHandle(); + } + + @Override + public int compare(T lhs, T rhs) { + if (mMyUser.equals(lhs.user)) { + return -1; + } else { + Long aUserSerial = mUserManager.getSerialNumberForUser(lhs.user); + Long bUserSerial = mUserManager.getSerialNumberForUser(rhs.user); + return aUserSerial.compareTo(bUserSerial); + } + } +} diff --git a/src/com/android/launcher3/model/AppNameComparator.java b/src/com/android/launcher3/model/AppNameComparator.java new file mode 100644 index 000000000..5f80037dc --- /dev/null +++ b/src/com/android/launcher3/model/AppNameComparator.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model; + +import android.content.Context; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.util.Thunk; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Class to manage access to an app name comparator. + * <p> + * Used to sort application name in all apps view and widget tray view. + */ +public class AppNameComparator { + private final Collator mCollator; + private final AbstractUserComparator<ItemInfo> mAppInfoComparator; + private final Comparator<String> mSectionNameComparator; + + public AppNameComparator(Context context) { + mCollator = Collator.getInstance(); + mAppInfoComparator = new AbstractUserComparator<ItemInfo>(context) { + + @Override + public final int compare(ItemInfo a, ItemInfo b) { + // Order by the title in the current locale + int result = compareTitles(a.title.toString(), b.title.toString()); + if (result == 0 && a instanceof AppInfo && b instanceof AppInfo) { + AppInfo aAppInfo = (AppInfo) a; + AppInfo bAppInfo = (AppInfo) b; + // If two apps have the same title, then order by the component name + result = aAppInfo.componentName.compareTo(bAppInfo.componentName); + if (result == 0) { + // If the two apps are the same component, then prioritize by the order that + // the app user was created (prioritizing the main user's apps) + return super.compare(a, b); + } + } + return result; + } + }; + mSectionNameComparator = new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return compareTitles(o1, o2); + } + }; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of applications. + */ + public Comparator<ItemInfo> getAppInfoComparator() { + return mAppInfoComparator; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of section names. + */ + public Comparator<String> getSectionNameComparator() { + return mSectionNameComparator; + } + + /** + * Compares two titles with the same return value semantics as Comparator. + */ + @Thunk int compareTitles(String titleA, String titleB) { + // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit + boolean aStartsWithLetter = (titleA.length() > 0) && + Character.isLetterOrDigit(titleA.codePointAt(0)); + boolean bStartsWithLetter = (titleB.length() > 0) && + Character.isLetterOrDigit(titleB.codePointAt(0)); + if (aStartsWithLetter && !bStartsWithLetter) { + return -1; + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1; + } + + // Order by the title in the current locale + return mCollator.compare(titleA, titleB); + } +} diff --git a/src/com/android/launcher3/model/MigrateFromRestoreTask.java b/src/com/android/launcher3/model/MigrateFromRestoreTask.java new file mode 100644 index 000000000..6a529f61f --- /dev/null +++ b/src/com/android/launcher3/model/MigrateFromRestoreTask.java @@ -0,0 +1,767 @@ +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.graphics.Point; +import android.text.TextUtils; +import android.util.Log; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherProvider; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class takes care of shrinking the workspace (by maximum of one row and one column), as a + * result of restoring from a larger device. + */ +public class MigrateFromRestoreTask { + + public static boolean ENABLED = false; + + private static final String TAG = "MigrateFromRestoreTask"; + private static final boolean DEBUG = true; + + private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size"; + private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size"; + + // These are carefully selected weights for various item types (Math.random?), to allow for + // the lease absurd migration experience. + private static final float WT_SHORTCUT = 1; + private static final float WT_APPLICATION = 0.8f; + private static final float WT_WIDGET_MIN = 2; + private static final float WT_WIDGET_FACTOR = 0.6f; + private static final float WT_FOLDER_FACTOR = 0.5f; + + private final Context mContext; + private final ContentValues mTempValues = new ContentValues(); + private final HashMap<String, Point> mWidgetMinSize; + private final InvariantDeviceProfile mIdp; + + private HashSet<String> mValidPackages; + public ArrayList<Long> mEntryToRemove; + private ArrayList<ContentProviderOperation> mUpdateOperations; + + private ArrayList<DbEntry> mCarryOver; + + private final int mSrcX, mSrcY; + @Thunk final int mTrgX, mTrgY; + private final boolean mShouldRemoveX, mShouldRemoveY; + + public MigrateFromRestoreTask(Context context) { + mContext = context; + + SharedPreferences prefs = prefs(context); + Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, "")); + mSrcX = sourceSize.x; + mSrcY = sourceSize.y; + + mWidgetMinSize = new HashMap<String, Point>(); + for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, + Collections.<String>emptySet())) { + String[] parts = s.split("#"); + mWidgetMinSize.put(parts[0], parsePoint(parts[1])); + } + + mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + mTrgX = mIdp.numColumns; + mTrgY = mIdp.numRows; + mShouldRemoveX = mTrgX < mSrcX; + mShouldRemoveY = mTrgY < mSrcY; + } + + public void execute() throws Exception { + mEntryToRemove = new ArrayList<>(); + mCarryOver = new ArrayList<>(); + mUpdateOperations = new ArrayList<>(); + + // Initialize list of valid packages. This contain all the packages which are already on + // the device and packages which are being installed. Any item which doesn't belong to + // this set is removed. + // Since the loader removes such items anyway, removing these items here doesn't cause any + // extra data loss and gives us more free space on the grid for better migration. + mValidPackages = new HashSet<>(); + for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) { + mValidPackages.add(info.packageName); + } + mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext) + .updateAndGetActiveSessionCache().keySet()); + + ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + if (allScreens.isEmpty()) { + throw new Exception("Unable to get workspace screens"); + } + + for (long screenId : allScreens) { + if (DEBUG) { + Log.d(TAG, "Migrating " + screenId); + } + migrateScreen(screenId); + } + + if (!mCarryOver.isEmpty()) { + LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); + for (DbEntry e : mCarryOver) { + itemMap.put(e.id, e); + } + + do { + // Some items are still remaining. Try adding a few new screens. + + // At every iteration, make sure that at least one item is removed from + // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed, + // break the loop and abort migration by throwing an exception. + OptimalPlacementSolution placement = new OptimalPlacementSolution( + new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true); + placement.find(); + if (placement.finalPlacedItems.size() > 0) { + long newScreenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); + allScreens.add(newScreenId); + for (DbEntry item : placement.finalPlacedItems) { + if (!mCarryOver.remove(itemMap.get(item.id))) { + throw new Exception("Unable to find matching items"); + } + item.screenId = newScreenId; + update(item); + } + } else { + throw new Exception("None of the items can be placed on an empty screen"); + } + + } while (!mCarryOver.isEmpty()); + + + LauncherAppState.getInstance().getModel() + .updateWorkspaceScreenOrder(mContext, allScreens); + } + + // Update items + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); + + if (!mEntryToRemove.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + } + mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, mEntryToRemove), null); + } + + // Make sure we haven't removed everything. + final Cursor c = mContext.getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); + boolean hasData = c.moveToNext(); + c.close(); + if (!hasData) { + throw new Exception("Removed every thing during grid resize"); + } + } + + /** + * Migrate a particular screen id. + * Strategy: + * 1) For all possible combinations of row and column, pick the one which causes the least + * data loss: {@link #tryRemove(int, int, ArrayList, float[])} + * 2) Maintain a list of all lost items before this screen, and add any new item lost from + * this screen to that list as well. + * 3) If all those items from the above list can be placed on this screen, place them + * (otherwise they are placed on a new screen). + */ + private void migrateScreen(long screenId) { + ArrayList<DbEntry> items = loadEntries(screenId); + + int removedCol = Integer.MAX_VALUE; + int removedRow = Integer.MAX_VALUE; + + // removeWt represents the cost function for loss of items during migration, and moveWt + // represents the cost function for repositioning the items. moveWt is only considered if + // removeWt is same for two different configurations. + // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least + // cost. + float removeWt = Float.MAX_VALUE; + float moveWt = Float.MAX_VALUE; + float[] outLoss = new float[2]; + ArrayList<DbEntry> finalItems = null; + + // Try removing all possible combinations + for (int x = 0; x < mSrcX; x++) { + for (int y = 0; y < mSrcY; y++) { + // Use a deep copy when trying out a particular combination as it can change + // the underlying object. + ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss); + + if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) { + removeWt = outLoss[0]; + moveWt = outLoss[1]; + removedCol = mShouldRemoveX ? x : removedCol; + removedRow = mShouldRemoveY ? y : removedRow; + finalItems = itemsOnScreen; + } + + // No need to loop over all rows, if a row removal is not needed. + if (!mShouldRemoveY) { + break; + } + } + + if (!mShouldRemoveX) { + break; + } + } + + if (DEBUG) { + Log.d(TAG, String.format("Removing row %d, column %d on screen %d", + removedRow, removedCol, screenId)); + } + + LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); + for (DbEntry e : deepCopy(items)) { + itemMap.put(e.id, e); + } + + for (DbEntry item : finalItems) { + DbEntry org = itemMap.get(item.id); + itemMap.remove(item.id); + + // Check if update is required + if (!item.columnsSame(org)) { + update(item); + } + } + + // The remaining items in {@link #itemMap} are those which didn't get placed. + for (DbEntry item : itemMap) { + mCarryOver.add(item); + } + + if (!mCarryOver.isEmpty() && removeWt == 0) { + // No new items were removed in this step. Try placing all the items on this screen. + boolean[][] occupied = new boolean[mTrgX][mTrgY]; + for (DbEntry item : finalItems) { + markCells(occupied, item, true); + } + + OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, + deepCopy(mCarryOver), true); + placement.find(); + if (placement.lowestWeightLoss == 0) { + // All items got placed + + for (DbEntry item : placement.finalPlacedItems) { + item.screenId = screenId; + update(item); + } + + mCarryOver.clear(); + } + } + } + + /** + * Updates an item in the DB. + */ + private void update(DbEntry item) { + mTempValues.clear(); + item.addToContentValues(mTempValues); + mUpdateOperations.add(ContentProviderOperation + .newUpdate(LauncherSettings.Favorites.getContentUri(item.id)) + .withValues(mTempValues).build()); + } + + /** + * Tries the remove the provided row and column. + * @param items all the items on the screen under operation + * @param outLoss array of size 2. The first entry is filled with weight loss, and the second + * with the overall item movement. + */ + private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items, + float[] outLoss) { + boolean[][] occupied = new boolean[mTrgX][mTrgY]; + + col = mShouldRemoveX ? col : Integer.MAX_VALUE; + row = mShouldRemoveY ? row : Integer.MAX_VALUE; + + ArrayList<DbEntry> finalItems = new ArrayList<>(); + ArrayList<DbEntry> removedItems = new ArrayList<>(); + + for (DbEntry item : items) { + if ((item.cellX <= col && (item.spanX + item.cellX) > col) + || (item.cellY <= row && (item.spanY + item.cellY) > row)) { + removedItems.add(item); + if (item.cellX >= col) item.cellX --; + if (item.cellY >= row) item.cellY --; + } else { + if (item.cellX > col) item.cellX --; + if (item.cellY > row) item.cellY --; + finalItems.add(item); + markCells(occupied, item, true); + } + } + + OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems); + placement.find(); + finalItems.addAll(placement.finalPlacedItems); + outLoss[0] = placement.lowestWeightLoss; + outLoss[1] = placement.lowestMoveCost; + return finalItems; + } + + @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) { + for (int i = item.cellX; i < (item.cellX + item.spanX); i++) { + for (int j = item.cellY; j < (item.cellY + item.spanY); j++) { + occupied[i][j] = val; + } + } + } + + @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { + if (x + w > mTrgX) return false; + if (y + h > mTrgY) return false; + + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + if (occupied[i + x][j + y]) { + return false; + } + } + } + return true; + } + + private class OptimalPlacementSolution { + private final ArrayList<DbEntry> itemsToPlace; + private final boolean[][] occupied; + + // If set to true, item movement are not considered in move cost, leading to a more + // linear placement. + private final boolean ignoreMove; + + float lowestWeightLoss = Float.MAX_VALUE; + float lowestMoveCost = Float.MAX_VALUE; + ArrayList<DbEntry> finalPlacedItems; + + public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) { + this(occupied, itemsToPlace, false); + } + + public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace, + boolean ignoreMove) { + this.occupied = occupied; + this.itemsToPlace = itemsToPlace; + this.ignoreMove = ignoreMove; + + // Sort the items such that larger widgets appear first followed by 1x1 items + Collections.sort(this.itemsToPlace); + } + + public void find() { + find(0, 0, 0, new ArrayList<DbEntry>()); + } + + /** + * Recursively finds a placement for the provided items. + * @param index the position in {@link #itemsToPlace} to start looking at. + * @param weightLoss total weight loss upto this point + * @param moveCost total move cost upto this point + * @param itemsPlaced all the items already placed upto this point + */ + public void find(int index, float weightLoss, float moveCost, + ArrayList<DbEntry> itemsPlaced) { + if ((weightLoss >= lowestWeightLoss) || + ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) { + // Abort, as we already have a better solution. + return; + + } else if (index >= itemsToPlace.size()) { + // End loop. + lowestWeightLoss = weightLoss; + lowestMoveCost = moveCost; + + // Keep a deep copy of current configuration as it can change during recursion. + finalPlacedItems = deepCopy(itemsPlaced); + return; + } + + DbEntry me = itemsToPlace.get(index); + int myX = me.cellX; + int myY = me.cellY; + + // List of items to pass over if this item was placed. + ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1); + itemsIncludingMe.addAll(itemsPlaced); + itemsIncludingMe.add(me); + + if (me.spanX > 1 || me.spanY > 1) { + // If the current item is a widget (and it greater than 1x1), try to place it at + // all possible positions. This is because a widget placed at one position can + // affect the placement of a different widget. + int myW = me.spanX; + int myH = me.spanY; + + for (int y = 0; y < mTrgY; y++) { + for (int x = 0; x < mTrgX; x++) { + float newMoveCost = moveCost; + if (x != myX) { + me.cellX = x; + newMoveCost ++; + } + if (y != myY) { + me.cellY = y; + newMoveCost ++; + } + if (ignoreMove) { + newMoveCost = moveCost; + } + + if (isVacant(occupied, x, y, myW, myH)) { + // place at this position and continue search. + markCells(occupied, me, true); + find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); + markCells(occupied, me, false); + } + + // Try resizing horizontally + if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) { + me.spanX --; + markCells(occupied, me, true); + // 1 extra move cost + find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); + markCells(occupied, me, false); + me.spanX ++; + } + + // Try resizing vertically + if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) { + me.spanY --; + markCells(occupied, me, true); + // 1 extra move cost + find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); + markCells(occupied, me, false); + me.spanY ++; + } + + // Try resizing horizontally & vertically + if (myH > me.minSpanY && myW > me.minSpanX && + isVacant(occupied, x, y, myW - 1, myH - 1)) { + me.spanX --; + me.spanY --; + markCells(occupied, me, true); + // 2 extra move cost + find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe); + markCells(occupied, me, false); + me.spanX ++; + me.spanY ++; + } + me.cellX = myX; + me.cellY = myY; + } + } + + // Finally also try a solution when this item is not included. Trying it in the end + // causes it to get skipped in most cases due to higher weight loss, and prevents + // unnecessary deep copies of various configurations. + find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); + } else { + // Since this is a 1x1 item and all the following items are also 1x1, just place + // it at 'the most appropriate position' and hope for the best. + // The most appropriate position: one with lease straight line distance + int newDistance = Integer.MAX_VALUE; + int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE; + + for (int y = 0; y < mTrgY; y++) { + for (int x = 0; x < mTrgX; x++) { + if (!occupied[x][y]) { + int dist = ignoreMove ? 0 : + ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y)); + if (dist < newDistance) { + newX = x; + newY = y; + newDistance = dist; + } + } + } + } + + if (newX < mTrgX && newY < mTrgY) { + float newMoveCost = moveCost; + if (newX != myX) { + me.cellX = newX; + newMoveCost ++; + } + if (newY != myY) { + me.cellY = newY; + newMoveCost ++; + } + if (ignoreMove) { + newMoveCost = moveCost; + } + markCells(occupied, me, true); + find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); + markCells(occupied, me, false); + me.cellX = myX; + me.cellY = myY; + + // Try to find a solution without this item, only if + // 1) there was at least one space, i.e., we were able to place this item + // 2) if the next item has the same weight (all items are already sorted), as + // if it has lower weight, that solution will automatically get discarded. + // 3) ignoreMove false otherwise, move cost is ignored and the weight will + // anyway be same. + if (index + 1 < itemsToPlace.size() + && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) { + find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); + } + } else { + // No more space. Jump to the end. + for (int i = index + 1; i < itemsToPlace.size(); i++) { + weightLoss += itemsToPlace.get(i).weight; + } + find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced); + } + } + } + } + + /** + * Loads entries for a particular screen id. + */ + public ArrayList<DbEntry> loadEntries(long screen) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[] { + Favorites._ID, // 0 + Favorites.ITEM_TYPE, // 1 + Favorites.CELLX, // 2 + Favorites.CELLY, // 3 + Favorites.SPANX, // 4 + Favorites.SPANY, // 5 + Favorites.INTENT, // 6 + Favorites.APPWIDGET_PROVIDER}, // 7 + Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP + + " AND " + Favorites.SCREEN + " = " + screen, null, null, null); + + final int indexId = c.getColumnIndexOrThrow(Favorites._ID); + final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); + final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); + final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); + final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); + final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); + final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + + ArrayList<DbEntry> entries = new ArrayList<>(); + while (c.moveToNext()) { + DbEntry entry = new DbEntry(); + entry.id = c.getLong(indexId); + entry.itemType = c.getInt(indexItemType); + entry.cellX = c.getInt(indexCellX); + entry.cellY = c.getInt(indexCellY); + entry.spanX = c.getInt(indexSpanX); + entry.spanY = c.getInt(indexSpanY); + entry.screenId = screen; + + try { + // calculate weight + switch (entry.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + verifyIntent(c.getString(indexIntent)); + entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT + ? WT_SHORTCUT : WT_APPLICATION; + break; + } + case Favorites.ITEM_TYPE_APPWIDGET: { + String provider = c.getString(indexAppWidgetProvider); + ComponentName cn = ComponentName.unflattenFromString(provider); + verifyPackage(cn.getPackageName()); + entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR + * entry.spanX * entry.spanY); + + // Migration happens for current user only. + LauncherAppWidgetProviderInfo pInfo = LauncherModel.getProviderInfo( + mContext, cn, UserHandleCompat.myUserHandle()); + Point spans = pInfo == null ? + mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext); + if (spans != null) { + entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; + entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; + } else { + // Assume that the widget be resized down to 2x2 + entry.minSpanX = entry.minSpanY = 2; + } + + if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { + throw new Exception("Widget can't be resized down to fit the grid"); + } + break; + } + case Favorites.ITEM_TYPE_FOLDER: { + int total = getFolderItemsCount(entry.id); + if (total == 0) { + throw new Exception("Folder is empty"); + } + entry.weight = WT_FOLDER_FACTOR * total; + break; + } + default: + throw new Exception("Invalid item type"); + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Removing item " + entry.id, e); + } + mEntryToRemove.add(entry.id); + continue; + } + + entries.add(entry); + } + return entries; + } + + /** + * @return the number of valid items in the folder. + */ + private int getFolderItemsCount(long folderId) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[] {Favorites._ID, Favorites.INTENT}, + Favorites.CONTAINER + " = " + folderId, null, null, null); + + int total = 0; + while (c.moveToNext()) { + try { + verifyIntent(c.getString(1)); + total++; + } catch (Exception e) { + mEntryToRemove.add(c.getLong(0)); + } + } + + return total; + } + + /** + * Verifies if the intent should be restored. + */ + private void verifyIntent(String intentStr) throws Exception { + Intent intent = Intent.parseUri(intentStr, 0); + if (intent.getComponent() != null) { + verifyPackage(intent.getComponent().getPackageName()); + } else if (intent.getPackage() != null) { + // Only verify package if the component was null. + verifyPackage(intent.getPackage()); + } + } + + /** + * Verifies if the package should be restored + */ + private void verifyPackage(String packageName) throws Exception { + if (!mValidPackages.contains(packageName)) { + throw new Exception("Package not available"); + } + } + + private static class DbEntry extends ItemInfo implements Comparable<DbEntry> { + + public float weight; + + public DbEntry() { } + + public DbEntry copy() { + DbEntry entry = new DbEntry(); + entry.copyFrom(this); + entry.weight = weight; + entry.minSpanX = minSpanX; + entry.minSpanY = minSpanY; + return entry; + } + + /** + * Comparator such that larger widgets come first, followed by all 1x1 items + * based on their weights. + */ + @Override + public int compareTo(DbEntry another) { + if (itemType == Favorites.ITEM_TYPE_APPWIDGET) { + if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { + return another.spanY * another.spanX - spanX * spanY; + } else { + return -1; + } + } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { + return 1; + } else { + // Place higher weight before lower weight. + return Float.compare(another.weight, weight); + } + } + + public boolean columnsSame(DbEntry org) { + return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX && + org.spanY == spanY && org.screenId == screenId; + } + + public void addToContentValues(ContentValues values) { + values.put(LauncherSettings.Favorites.SCREEN, screenId); + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + values.put(LauncherSettings.Favorites.SPANX, spanX); + values.put(LauncherSettings.Favorites.SPANY, spanY); + } + } + + @Thunk static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) { + ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size()); + for (DbEntry e : src) { + dup.add(e.copy()); + } + return dup; + } + + private static Point parsePoint(String point) { + String[] split = point.split(","); + return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1])); + } + + public static void markForMigration(Context context, int srcX, int srcY, + HashSet<String> widgets) { + prefs(context).edit() + .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY) + .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets) + .apply(); + } + + public static boolean shouldRunTask(Context context) { + return !TextUtils.isEmpty(prefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, "")); + } + + public static void clearFlags(Context context) { + prefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE) + .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); + } + + private static SharedPreferences prefs(Context context) { + return context.getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + } +} diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/PackageItemInfo.java new file mode 100644 index 000000000..30f228c68 --- /dev/null +++ b/src/com/android/launcher3/model/PackageItemInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import android.graphics.Bitmap; + +import com.android.launcher3.ItemInfo; + +import java.util.Arrays; + +/** + * Represents a {@link Package} in the widget tray section. + */ +public class PackageItemInfo extends ItemInfo { + + /** + * A bitmap version of the application icon. + */ + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon. + */ + public boolean usingLowResIcon; + + /** + * Package name of the {@link ItemInfo}. + */ + public String packageName; + + /** + * Character that is used as a section name for the {@link ItemInfo#title}. + * (e.g., "G" will be stored if title is "Google") + */ + public String titleSectionName; + + int flags = 0; + + PackageItemInfo(String packageName) { + this.packageName = packageName; + } + + @Override + public String toString() { + return "PackageItemInfo(title=" + title + " id=" + this.id + + " type=" + this.itemType + " container=" + this.container + + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; + } +} diff --git a/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java new file mode 100644 index 000000000..b99056023 --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java @@ -0,0 +1,97 @@ +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.ComponentKey; + +import java.text.Collator; +import java.util.Comparator; +import java.util.HashMap; + +public class WidgetsAndShortcutNameComparator implements Comparator<Object> { + private final AppWidgetManagerCompat mManager; + private final PackageManager mPackageManager; + private final HashMap<ComponentKey, String> mLabelCache; + private final Collator mCollator; + private final UserHandleCompat mMainHandle; + + public WidgetsAndShortcutNameComparator(Context context) { + mManager = AppWidgetManagerCompat.getInstance(context); + mPackageManager = context.getPackageManager(); + mLabelCache = new HashMap<>(); + mCollator = Collator.getInstance(); + mMainHandle = UserHandleCompat.myUserHandle(); + } + + /** + * Resets any stored state. + */ + public void reset() { + mLabelCache.clear(); + } + + @Override + public final int compare(Object objA, Object objB) { + ComponentKey keyA = getComponentKey(objA); + ComponentKey keyB = getComponentKey(objB); + + // Independent of how the labels compare, if only one of the two widget info belongs to + // work profile, put that one in the back. + boolean aWorkProfile = !mMainHandle.equals(keyA.user); + boolean bWorkProfile = !mMainHandle.equals(keyB.user); + if (aWorkProfile && !bWorkProfile) { + return 1; + } + if (!aWorkProfile && bWorkProfile) { + return -1; + } + + // Get the labels for comparison + String labelA = mLabelCache.get(keyA); + String labelB = mLabelCache.get(keyB); + if (labelA == null) { + labelA = getLabel(objA); + mLabelCache.put(keyA, labelA); + } + if (labelB == null) { + labelB = getLabel(objB); + mLabelCache.put(keyB, labelB); + } + return mCollator.compare(labelA, labelB); + } + + /** + * @return a component key for the given widget or shortcut info. + */ + private ComponentKey getComponentKey(Object o) { + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + return new ComponentKey(widgetInfo.provider, mManager.getUser(widgetInfo)); + } else { + ResolveInfo shortcutInfo = (ResolveInfo) o; + ComponentName cn = new ComponentName(shortcutInfo.activityInfo.packageName, + shortcutInfo.activityInfo.name); + // Currently, there are no work profile shortcuts + return new ComponentKey(cn, UserHandleCompat.myUserHandle()); + } + } + + /** + * @return the label for the given widget or shortcut info. This may be an expensive call. + */ + private String getLabel(Object o) { + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + return Utilities.trim(mManager.loadLabel(widgetInfo)); + } else { + ResolveInfo shortcutInfo = (ResolveInfo) o; + return Utilities.trim(shortcutInfo.loadLabel(mPackageManager)); + } + } +}; diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java new file mode 100644 index 000000000..eef4f9173 --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -0,0 +1,191 @@ + +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.util.Log; + +import com.android.launcher3.AppFilter; +import com.android.launcher3.IconCache; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * Widgets data model that is used by the adapters of the widget views and controllers. + * + * <p> The widgets and shortcuts are organized using package name as its index. + */ +public class WidgetsModel { + + private static final String TAG = "WidgetsModel"; + private static final boolean DEBUG = false; + + /* List of packages that is tracked by this model. */ + private ArrayList<PackageItemInfo> mPackageItemInfos = new ArrayList<>(); + + /* Map of widgets and shortcuts that are tracked per package. */ + private HashMap<PackageItemInfo, ArrayList<Object>> mWidgetsList = new HashMap<>(); + + private ArrayList<Object> mRawList; + + private final AppWidgetManagerCompat mAppWidgetMgr; + private final WidgetsAndShortcutNameComparator mWidgetAndShortcutNameComparator; + private final Comparator<ItemInfo> mAppNameComparator; + private final IconCache mIconCache; + private final AppFilter mAppFilter; + private AlphabeticIndexCompat mIndexer; + + public WidgetsModel(Context context, IconCache iconCache, AppFilter appFilter) { + mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context); + mWidgetAndShortcutNameComparator = new WidgetsAndShortcutNameComparator(context); + mAppNameComparator = (new AppNameComparator(context)).getAppInfoComparator(); + mIconCache = iconCache; + mAppFilter = appFilter; + mIndexer = new AlphabeticIndexCompat(context); + } + + @SuppressWarnings("unchecked") + private WidgetsModel(WidgetsModel model) { + mAppWidgetMgr = model.mAppWidgetMgr; + mPackageItemInfos = (ArrayList<PackageItemInfo>) model.mPackageItemInfos.clone(); + mWidgetsList = (HashMap<PackageItemInfo, ArrayList<Object>>) model.mWidgetsList.clone(); + mRawList = (ArrayList<Object>) model.mRawList.clone(); + mWidgetAndShortcutNameComparator = model.mWidgetAndShortcutNameComparator; + mAppNameComparator = model.mAppNameComparator; + mIconCache = model.mIconCache; + mAppFilter = model.mAppFilter; + } + + // Access methods that may be deleted if the private fields are made package-private. + public int getPackageSize() { + if (mPackageItemInfos == null) { + return 0; + } + return mPackageItemInfos.size(); + } + + // Access methods that may be deleted if the private fields are made package-private. + public PackageItemInfo getPackageItemInfo(int pos) { + if (pos >= mPackageItemInfos.size() || pos < 0) { + return null; + } + return mPackageItemInfos.get(pos); + } + + public List<Object> getSortedWidgets(int pos) { + return mWidgetsList.get(mPackageItemInfos.get(pos)); + } + + public ArrayList<Object> getRawList() { + return mRawList; + } + + public void setWidgetsAndShortcuts(ArrayList<Object> rawWidgetsShortcuts) { + Utilities.assertWorkerThread(); + mRawList = rawWidgetsShortcuts; + if (DEBUG) { + Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size()); + } + + // Temporary list for {@link PackageItemInfos} to avoid having to go through + // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList} + HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>(); + + // clear the lists. + mWidgetsList.clear(); + mPackageItemInfos.clear(); + mWidgetAndShortcutNameComparator.reset(); + + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + + // add and update. + for (Object o: rawWidgetsShortcuts) { + String packageName = ""; + UserHandleCompat userHandle = null; + ComponentName componentName = null; + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + + // Ensure that all widgets we show can be added on a workspace of this size + int minSpanX = Math.min(widgetInfo.spanX, widgetInfo.minSpanX); + int minSpanY = Math.min(widgetInfo.spanY, widgetInfo.minSpanY); + if (minSpanX <= (int) idp.numColumns && + minSpanY <= (int) idp.numRows) { + componentName = widgetInfo.provider; + packageName = widgetInfo.provider.getPackageName(); + userHandle = mAppWidgetMgr.getUser(widgetInfo); + } else { + if (DEBUG) { + Log.d(TAG, String.format( + "Widget %s : (%d X %d) can't fit on this device", + widgetInfo.provider, minSpanX, minSpanY)); + } + continue; + } + } else if (o instanceof ResolveInfo) { + ResolveInfo resolveInfo = (ResolveInfo) o; + componentName = new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + packageName = resolveInfo.activityInfo.packageName; + userHandle = UserHandleCompat.myUserHandle(); + } + + if (componentName == null || userHandle == null) { + Log.e(TAG, String.format("Widget cannot be set for %s.", o.getClass().toString())); + continue; + } + if (mAppFilter != null && !mAppFilter.shouldShowApp(componentName)) { + if (DEBUG) { + Log.d(TAG, String.format("%s is filtered and not added to the widget tray.", + packageName)); + } + continue; + } + + PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName); + ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo); + if (widgetsShortcutsList != null) { + widgetsShortcutsList.add(o); + } else { + widgetsShortcutsList = new ArrayList<>(); + widgetsShortcutsList.add(o); + pInfo = new PackageItemInfo(packageName); + mIconCache.getTitleAndIconForApp(packageName, userHandle, + true /* userLowResIcon */, pInfo); + pInfo.titleSectionName = mIndexer.computeSectionName(pInfo.title); + mWidgetsList.put(pInfo, widgetsShortcutsList); + tmpPackageItemInfos.put(packageName, pInfo); + mPackageItemInfos.add(pInfo); + } + } + + // sort. + Collections.sort(mPackageItemInfos, mAppNameComparator); + for (PackageItemInfo p: mPackageItemInfos) { + Collections.sort(mWidgetsList.get(p), mWidgetAndShortcutNameComparator); + } + } + + /** + * Create a snapshot of the widgets model. + * <p> + * Usage case: view binding without being modified from package updates. + */ + @Override + public WidgetsModel clone(){ + return new WidgetsModel(this); + } +}
\ No newline at end of file |