[DO NOT MERGE] Address some spacing issues in all apps related to new support library
am: d797327 -s ours
* commit 'd797327b79c5ded53f698201af5abb7d779ddfa9':
[DO NOT MERGE] Address some spacing issues in all apps related to new support library
Change-Id: I8c38d1c542f1143dd7c9f330795229b4a03c7ac8
diff --git a/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml b/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml
index 8e349b7..2b39b09 100644
--- a/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml
+++ b/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml
@@ -28,5 +28,6 @@
android:drawableLeft="@drawable/ic_actionbar_accept"
android:drawablePadding="8dp"
android:gravity="start|center_vertical"
- android:text="@string/wallpaper_instructions">
+ android:text="@string/wallpaper_instructions"
+ android:enabled="false">
</com.android.launcher3.AlphaDisableableButton>
diff --git a/WallpaperPicker/res/values/arrays.xml b/WallpaperPicker/res/values/arrays.xml
new file mode 100644
index 0000000..5c10b98
--- /dev/null
+++ b/WallpaperPicker/res/values/arrays.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<resources>
+ <string-array name="which_wallpaper_options">
+ <item>@string/which_wallpaper_option_home_screen</item>
+ <item>@string/which_wallpaper_option_lock_screen</item>
+ <item>@string/which_wallpaper_option_home_screen_and_lock_screen</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/WallpaperPicker/res/values/strings.xml b/WallpaperPicker/res/values/strings.xml
index 2bfd476..aef6a1a 100644
--- a/WallpaperPicker/res/values/strings.xml
+++ b/WallpaperPicker/res/values/strings.xml
@@ -53,4 +53,11 @@
<string name="pick_wallpaper">Wallpapers</string>
<!-- Title of activity for cropping wallpapers -->
<string name="crop_wallpaper">Crop wallpaper</string>
+
+ <!-- Option for setting the wallpaper only on the home screen. -->
+ <string name="which_wallpaper_option_home_screen">Home screen</string>
+ <!-- Option for setting the wallpaper only on the lock screen. -->
+ <string name="which_wallpaper_option_lock_screen">Lock screen</string>
+ <!-- Option for setting the wallpaper on both the home screen and lock screen. -->
+ <string name="which_wallpaper_option_home_screen_and_lock_screen">Home screen and lock screen</string>
</resources>
diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
index 8b51447..027e947 100644
--- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
+++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
@@ -33,7 +33,9 @@
import android.util.Log;
import android.widget.Toast;
+import com.android.launcher3.NycWallpaperUtils;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
@@ -42,7 +44,7 @@
import java.io.IOException;
import java.io.InputStream;
-public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
+public class BitmapCropTask extends AsyncTask<Integer, Void, Boolean> {
public interface OnBitmapCroppedHandler {
public void onBitmapCropped(byte[] imageBytes);
@@ -71,15 +73,6 @@
BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler;
boolean mNoCrop;
- public BitmapCropTask(Context c, String filePath,
- RectF cropBounds, int rotation, int outWidth, int outHeight,
- boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
- mContext = c;
- mInFilePath = filePath;
- init(cropBounds, rotation,
- outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler);
- }
-
public BitmapCropTask(byte[] imageBytes,
RectF cropBounds, int rotation, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
@@ -175,7 +168,7 @@
public Bitmap getCroppedBitmap() {
return mCroppedBitmap;
}
- public boolean cropBitmap() {
+ public boolean cropBitmap(int whichWallpaper) {
boolean failure = false;
@@ -189,7 +182,11 @@
try {
InputStream is = regenerateInputStream();
if (is != null) {
- wallpaperManager.setStream(is);
+ if (!Utilities.isNycOrAbove()) {
+ wallpaperManager.setStream(is);
+ } else {
+ NycWallpaperUtils.setStream(mContext, is, null, true, whichWallpaper);
+ }
Utils.closeSilently(is);
}
} catch (IOException e) {
@@ -375,7 +372,13 @@
if (mSetWallpaper && wallpaperManager != null) {
try {
byte[] outByteArray = tmpOut.toByteArray();
- wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
+ if (!Utilities.isNycOrAbove()) {
+ wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
+ } else {
+ NycWallpaperUtils.setStream(mContext,
+ new ByteArrayInputStream(outByteArray), null, true,
+ whichWallpaper);
+ }
if (mOnBitmapCroppedHandler != null) {
mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
}
@@ -393,8 +396,8 @@
}
@Override
- protected Boolean doInBackground(Void... params) {
- return cropBitmap();
+ protected Boolean doInBackground(Integer... params) {
+ return cropBitmap(params.length == 0 ? NycWallpaperUtils.FLAG_SET_SYSTEM : params[0]);
}
@Override
diff --git a/WallpaperPicker/src/com/android/launcher3/NycWallpaperUtils.java b/WallpaperPicker/src/com/android/launcher3/NycWallpaperUtils.java
new file mode 100644
index 0000000..abac830
--- /dev/null
+++ b/WallpaperPicker/src/com/android/launcher3/NycWallpaperUtils.java
@@ -0,0 +1,79 @@
+package com.android.launcher3;
+
+import android.app.AlertDialog;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Utility class used to help set lockscreen wallpapers on N+.
+ */
+public class NycWallpaperUtils {
+ public static final int FLAG_SET_SYSTEM = 1 << 0; // TODO: use WallpaperManager.FLAG_SET_SYSTEM
+ public static final int FLAG_SET_LOCK = 1 << 1; // TODO: use WallpaperManager.FLAG_SET_LOCK
+
+ /**
+ * Calls cropTask.execute(), once the user has selected which wallpaper to set. On pre-N
+ * devices, the prompt is not displayed since there is no API to set the lockscreen wallpaper.
+ */
+ public static void executeCropTaskAfterPrompt(
+ Context context, final AsyncTask<Integer, ?, ?> cropTask,
+ DialogInterface.OnCancelListener onCancelListener) {
+ if (Utilities.isNycOrAbove()) {
+ new AlertDialog.Builder(context)
+ .setTitle(R.string.wallpaper_instructions)
+ .setItems(R.array.which_wallpaper_options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int selectedItemIndex) {
+ int whichWallpaper;
+ if (selectedItemIndex == 0) {
+ whichWallpaper = FLAG_SET_SYSTEM;
+ } else if (selectedItemIndex == 1) {
+ whichWallpaper = FLAG_SET_LOCK;
+ } else {
+ whichWallpaper = FLAG_SET_SYSTEM | FLAG_SET_LOCK;
+ }
+ cropTask.execute(whichWallpaper);
+ }
+ })
+ .setOnCancelListener(onCancelListener)
+ .show();
+ } else {
+ cropTask.execute(FLAG_SET_SYSTEM);
+ }
+ }
+
+ public static void setStream(Context context, final InputStream data, Rect visibleCropHint,
+ boolean allowBackup, int whichWallpaper) throws IOException {
+ WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
+ try {
+ // TODO: use mWallpaperManager.setStream(data, visibleCropHint, allowBackup, which)
+ // without needing reflection.
+ Method setStream = WallpaperManager.class.getMethod("setStream", InputStream.class,
+ Rect.class, boolean.class, int.class);
+ setStream.invoke(wallpaperManager, data, visibleCropHint, allowBackup, whichWallpaper);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ // Fall back to previous implementation (set system)
+ wallpaperManager.setStream(data);
+ }
+ }
+
+ public static void clear(Context context, int whichWallpaper) throws IOException {
+ WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
+ try {
+ // TODO: use mWallpaperManager.clear(whichWallpaper) without needing reflection.
+ Method clear = WallpaperManager.class.getMethod("clear", int.class);
+ clear.invoke(wallpaperManager, whichWallpaper);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Fall back to previous implementation (clear system)
+ wallpaperManager.clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
index 75bdb8a..364cdb8 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
@@ -21,6 +21,7 @@
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -86,6 +87,18 @@
@Thunk Set<Bitmap> mReusableBitmaps =
Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
+ private final DialogInterface.OnCancelListener mOnDialogCancelListener =
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ getActionBar().show();
+ View wallpaperStrip = findViewById(R.id.wallpaper_strip);
+ if (wallpaperStrip != null) {
+ wallpaperStrip.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -242,6 +255,10 @@
}
}
+ public DialogInterface.OnCancelListener getOnDialogCancelListener() {
+ return mOnDialogCancelListener;
+ }
+
protected void onLoadRequestComplete(LoadRequest req, boolean success) {
mCurrentLoadRequest = null;
if (success) {
@@ -326,7 +343,7 @@
};
cropTask.setOnEndRunnable(onEndCrop);
cropTask.setNoCrop(true);
- cropTask.execute();
+ NycWallpaperUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener());
}
protected void cropImageAndSetWallpaper(Resources res, int resId,
@@ -335,8 +352,7 @@
// this device
int rotation = BitmapUtils.getRotationFromExif(res, resId);
Point inSize = mCropView.getSourceDimensions();
- Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
- getWindowManager());
+ Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), getWindowManager());
RectF crop = Utils.getMaxCropRect(
inSize.x, inSize.y, outSize.x, outSize.y, false);
BitmapCropTask.OnEndCropHandler onEndCrop = new BitmapCropTask.OnEndCropHandler() {
@@ -355,7 +371,7 @@
};
BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId,
crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
- cropTask.execute();
+ NycWallpaperUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener());
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
index 5d41238..5c6113f 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
@@ -44,6 +44,7 @@
import android.os.Bundle;
import android.os.Process;
import android.provider.MediaStore;
+import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import android.view.ActionMode;
@@ -77,13 +78,15 @@
import com.android.photos.BitmapRegionTileSource;
import com.android.photos.BitmapRegionTileSource.BitmapSource;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
public class WallpaperPickerActivity extends WallpaperCropActivity {
- static final String TAG = "Launcher.WallpaperPickerActivity";
+ static final String TAG = "WallpaperPickerActivity";
public static final int IMAGE_PICK = 5;
public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6;
@@ -317,15 +320,75 @@
a.onLoadRequestComplete(req, true);
}
@Override
- public void onSave(WallpaperPickerActivity a) {
- try {
- WallpaperManager.getInstance(a.getContext()).clear();
- a.setResult(Activity.RESULT_OK);
- } catch (IOException e) {
- Log.w("Setting wallpaper to default threw exception", e);
+ public void onSave(final WallpaperPickerActivity a) {
+ if (!Utilities.isNycOrAbove()) {
+ try {
+ WallpaperManager.getInstance(a.getContext()).clear();
+ a.setResult(Activity.RESULT_OK);
+ } catch (IOException e) {
+ Log.e(TAG, "Setting wallpaper to default threw exception", e);
+ } catch (SecurityException e) {
+ Log.w(TAG, "Setting wallpaper to default threw exception", e);
+ // In this case, clearing worked; the exception was thrown afterwards.
+ a.setResult(Activity.RESULT_OK);
+ }
+ a.finish();
+ } else {
+ BitmapCropTask.OnEndCropHandler onEndCropHandler
+ = new BitmapCropTask.OnEndCropHandler() {
+ @Override
+ public void run(boolean cropSucceeded) {
+ if (cropSucceeded) {
+ a.setResult(Activity.RESULT_OK);
+ }
+ a.finish();
+ }
+ };
+ BitmapCropTask setWallpaperTask = getDefaultWallpaperCropTask(a, onEndCropHandler);
+
+ NycWallpaperUtils.executeCropTaskAfterPrompt(a, setWallpaperTask,
+ a.getOnDialogCancelListener());
}
- a.finish();
}
+
+ @NonNull
+ private BitmapCropTask getDefaultWallpaperCropTask(final WallpaperPickerActivity a,
+ final BitmapCropTask.OnEndCropHandler onEndCropHandler) {
+ return new BitmapCropTask(a, null, null, -1, -1, -1,
+ true, false, onEndCropHandler) {
+ @Override
+ protected Boolean doInBackground(Integer... params) {
+ int whichWallpaper = params[0];
+ boolean succeeded = true;
+ try {
+ if (whichWallpaper == NycWallpaperUtils.FLAG_SET_LOCK) {
+ Bitmap defaultWallpaper = ((BitmapDrawable) WallpaperManager
+ .getInstance(a.getApplicationContext()).getBuiltInDrawable())
+ .getBitmap();
+ ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+ if (defaultWallpaper.compress(Bitmap.CompressFormat.PNG, 100,
+ tmpOut)) {
+ byte[] outByteArray = tmpOut.toByteArray();
+ NycWallpaperUtils.setStream(a.getApplicationContext(),
+ new ByteArrayInputStream(outByteArray), null, true,
+ NycWallpaperUtils.FLAG_SET_LOCK);
+ }
+ } else {
+ NycWallpaperUtils.clear(a, whichWallpaper);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Setting wallpaper to default threw exception", e);
+ succeeded = false;
+ } catch (SecurityException e) {
+ Log.w(TAG, "Setting wallpaper to default threw exception", e);
+ // In this case, clearing worked; the exception was thrown afterwards.
+ succeeded = true;
+ }
+ return succeeded;
+ }
+ };
+ }
+
@Override
public boolean isSelectable() {
return true;
@@ -442,10 +505,10 @@
}
return;
}
- setWallpaperButtonEnabled(true);
WallpaperTileInfo info = (WallpaperTileInfo) v.getTag();
if (info.isSelectable() && v.getVisibility() == View.VISIBLE) {
selectTile(v);
+ setWallpaperButtonEnabled(true);
}
info.onClick(WallpaperPickerActivity.this);
}
@@ -561,7 +624,7 @@
new View.OnClickListener() {
@Override
public void onClick(View v) {
- // Ensure that a tile is slelected and loaded.
+ // Ensure that a tile is selected and loaded.
if (mSelectedTile != null && mCropView.getTileSource() != null) {
// Prevent user from selecting any new tile.
mWallpaperStrip.setVisibility(View.GONE);
@@ -570,9 +633,9 @@
WallpaperTileInfo info = (WallpaperTileInfo) mSelectedTile.getTag();
info.onSave(WallpaperPickerActivity.this);
} else {
- // no tile was selected, so we just finish the activity and go back
- setResult(Activity.RESULT_OK);
- finish();
+ // This case shouldn't be possible, since "Set wallpaper" is disabled
+ // until user clicks on a title.
+ Log.w(TAG, "\"Set wallpaper\" was clicked when no tile was selected");
}
}
});
@@ -849,7 +912,7 @@
(int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned);
cropTask.setCropBounds(cropRect);
- if (cropTask.cropBitmap()) {
+ if (cropTask.cropBitmap(NycWallpaperUtils.FLAG_SET_SYSTEM)) {
return cropTask.getCroppedBitmap();
} else {
return null;
@@ -1195,11 +1258,6 @@
@Override
public boolean enableRotation() {
- // Check if rotation is enabled for this device.
- if (Utilities.isRotationAllowedForDevice(getContext()))
- return true;
-
- // Check if the user has specifically enabled rotation via preferences.
- return Utilities.isAllowRotationPrefEnabled(getApplicationContext(), true);
+ return true;
}
}
diff --git a/build.gradle b/build.gradle
index 44f5b7c..b9a990f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,7 @@
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:+'
+ classpath 'com.android.tools.build:gradle:1.5.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.7.0'
}
}
@@ -51,13 +51,13 @@
}
dependencies {
- compile 'com.android.support:support-v4:23.0.1'
- compile 'com.android.support:recyclerview-v7:23.0.1'
+ compile 'com.android.support:support-v4:23.1.1'
+ compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-2'
testCompile 'junit:junit:4.12'
- androidTestCompile 'com.android.support.test:runner:+'
- androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:+'
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}
protobuf {
diff --git a/proguard.flags b/proguard.flags
index 7725800..a167663 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -70,3 +70,10 @@
public float getBackgroundAlpha();
public void setBackgroundAlpha(float);
}
+
+# Proguard will strip new callbacks in LauncherApps.Callback from
+# WrappedCallback if compiled against an older SDK. Don't let this happen.
+-keep class com.android.launcher3.compat.** {
+ *;
+}
+
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index 3b25dca..c431593 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -22,6 +22,8 @@
import com.android.launcher3.compat.LauncherActivityInfoCompat;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.FlagOp;
+import com.android.launcher3.util.StringFilter;
import java.util.ArrayList;
import java.util.HashSet;
@@ -118,6 +120,21 @@
}
}
+ /**
+ * Updates the apps for the given packageName and user based on {@param op}.
+ */
+ public void updatePackageFlags(StringFilter pkgFilter, UserHandleCompat user, FlagOp op) {
+ final List<AppInfo> data = this.data;
+ for (int i = data.size() - 1; i >= 0; i--) {
+ AppInfo info = data.get(i);
+ final ComponentName component = info.intent.getComponent();
+ if (info.user.equals(user) && pkgFilter.matches(component.getPackageName())) {
+ info.isDisabled = op.apply(info.isDisabled);
+ modified.add(info);
+ }
+ }
+ }
+
public void updateIconsAndLabels(HashSet<String> packages, UserHandleCompat user,
ArrayList<AppInfo> outUpdates) {
for (AppInfo info : data) {
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index ede6c71..c7a6ae4 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -26,6 +26,7 @@
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageManagerHelper;
import java.util.ArrayList;
import java.util.Arrays;
@@ -58,6 +59,11 @@
int flags = 0;
+ /**
+ * {@see ShortcutInfo#isDisabled}
+ */
+ int isDisabled = ShortcutInfo.DEFAULT;
+
AppInfo() {
itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
}
@@ -75,10 +81,22 @@
*/
public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user,
IconCache iconCache) {
+ this(context, info, user, iconCache,
+ UserManagerCompat.getInstance(context).isQuietModeEnabled(user));
+ }
+
+ public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user,
+ IconCache iconCache, boolean quietModeEnabled) {
this.componentName = info.getComponentName();
this.container = ItemInfo.NO_ID;
-
flags = initFlags(info);
+ if (PackageManagerHelper.isAppSuspended(info.getApplicationInfo())) {
+ isDisabled |= ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+ }
+ if (quietModeEnabled) {
+ isDisabled |= ShortcutInfo.FLAG_DISABLED_QUIET_USER;
+ }
+
iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */);
intent = makeLaunchIntent(context, info, user);
this.user = user;
@@ -103,6 +121,7 @@
title = Utilities.trim(info.title);
intent = new Intent(info.intent);
flags = info.flags;
+ isDisabled = info.isDisabled;
iconBitmap = info.iconBitmap;
}
@@ -143,4 +162,9 @@
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
.putExtra(EXTRA_PROFILE, serialNumber);
}
+
+ @Override
+ public boolean isDisabled() {
+ return isDisabled != 0;
+ }
}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index f6c9e3c..09ec60c 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -150,7 +150,7 @@
Bitmap b = info.getIcon(iconCache);
FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b);
- if (info.isDisabled != 0) {
+ if (info.isDisabled()) {
iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
}
setIcon(iconDrawable, mIconSize);
@@ -166,7 +166,11 @@
}
public void applyFromApplicationInfo(AppInfo info) {
- setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize);
+ FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(info.iconBitmap);
+ if (info.isDisabled()) {
+ iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
+ }
+ setIcon(iconDrawable, mIconSize);
setText(info.title);
if (info.contentDescription != null) {
setContentDescription(info.contentDescription);
@@ -250,11 +254,11 @@
private void updateIconState() {
if (mIcon instanceof FastBitmapDrawable) {
FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
- if (isPressed() || mStayPressed) {
- d.animateState(FastBitmapDrawable.State.PRESSED);
- } else if (getTag() instanceof ShortcutInfo
- && ((ShortcutInfo) getTag()).isDisabled != 0) {
+ if (getTag() instanceof ItemInfo
+ && ((ItemInfo) getTag()).isDisabled()) {
d.animateState(FastBitmapDrawable.State.DISABLED);
+ } else if (isPressed() || mStayPressed) {
+ d.animateState(FastBitmapDrawable.State.PRESSED);
} else {
d.animateState(FastBitmapDrawable.State.NORMAL);
}
diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java
index eefb287..2f0bfe5 100644
--- a/src/com/android/launcher3/DragController.java
+++ b/src/com/android/launcher3/DragController.java
@@ -341,7 +341,8 @@
}
endDrag();
}
- public void onAppsRemoved(final ArrayList<String> packageNames, HashSet<ComponentName> cns) {
+
+ public void onAppsRemoved(final HashSet<String> packageNames, HashSet<ComponentName> cns) {
// Cancel the current drag if we are removing an app that we are dragging
if (mDragObject != null) {
Object rawDragInfo = mDragObject.dragInfo;
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 05ad538..c72c2f9 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -118,7 +118,7 @@
mUserManager = UserManagerCompat.getInstance(mContext);
mLauncherApps = LauncherAppsCompat.getInstance(mContext);
mIconDpi = inv.fillResIconDpi;
- mIconDb = new IconDB(context);
+ mIconDb = new IconDB(context, inv.iconBitmapSize);
mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
@@ -809,8 +809,10 @@
private final static String COLUMN_LABEL = "label";
private final static String COLUMN_SYSTEM_STATE = "system_state";
- public IconDB(Context context) {
- super(context, LauncherFiles.APP_ICONS_DB, RELEASE_VERSION, TABLE_NAME);
+ public IconDB(Context context, int iconPixelSize) {
+ super(context, LauncherFiles.APP_ICONS_DB,
+ (RELEASE_VERSION << 16) + iconPixelSize,
+ TABLE_NAME);
}
@Override
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 46b9b7d..921e90c 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -87,7 +87,7 @@
}
}
- public static void removeFromInstallQueue(Context context, ArrayList<String> packageNames,
+ public static void removeFromInstallQueue(Context context, HashSet<String> packageNames,
UserHandleCompat user) {
if (packageNames.isEmpty()) {
return;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index b3a8bbc..d601322 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -73,17 +73,17 @@
/**
* Number of icons inside the hotseat area.
*/
- int numHotseatIcons;
+ public int numHotseatIcons;
float hotseatIconSize;
int defaultLayoutId;
// Derived invariant properties
- int hotseatAllAppsRank;
+ public int hotseatAllAppsRank;
DeviceProfile landscapeProfile;
DeviceProfile portraitProfile;
- InvariantDeviceProfile() {
+ public InvariantDeviceProfile() {
}
public InvariantDeviceProfile(InvariantDeviceProfile p) {
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index f7e0ea4..c7a6807 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -197,4 +197,11 @@
+ " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos)
+ " user=" + user + ")";
}
+
+ /**
+ * Whether this item is disabled.
+ */
+ public boolean isDisabled() {
+ return false;
+ }
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 166acd3..ff120f7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -488,11 +488,11 @@
IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
registerReceiver(mCloseSystemDialogsReceiver, filter);
- mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext());
+ mRotationEnabled = getResources().getBoolean(R.bool.allow_rotation);
// In case we are on a device with locked rotation, we should look at preferences to check
// if the user has specifically allowed rotation.
if (!mRotationEnabled) {
- mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext(), false);
+ mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext());
}
// On large interfaces, or on devices that a user has specifically enabled screen rotation,
@@ -939,6 +939,9 @@
protected void onStop() {
super.onStop();
FirstFrameAnimatorHelper.setIsVisible(false);
+ if (Utilities.isNycOrAbove()) {
+ mAppWidgetHost.stopListening();
+ }
if (mLauncherCallbacks != null) {
mLauncherCallbacks.onStop();
@@ -949,6 +952,9 @@
protected void onStart() {
super.onStart();
FirstFrameAnimatorHelper.setIsVisible(true);
+ if (Utilities.isNycOrAbove()) {
+ mAppWidgetHost.startListening();
+ }
if (mLauncherCallbacks != null) {
mLauncherCallbacks.onStart();
@@ -1205,7 +1211,7 @@
} else {
// On devices with a locked orientation, we will at least have the allow rotation
// setting.
- return !Utilities.isRotationAllowedForDevice(this);
+ return !getResources().getBoolean(R.bool.allow_rotation);
}
}
@@ -1594,8 +1600,7 @@
ItemInfo info = mPendingAddInfo;
if (appWidgetInfo == null) {
- appWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(this,
- mAppWidgetManager.getAppWidgetInfo(appWidgetId));
+ appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
}
if (appWidgetInfo.isCustomWidget) {
@@ -2591,10 +2596,10 @@
final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
if (v.isReadyForClickSetup()) {
int widgetId = info.appWidgetId;
- AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId);
+ LauncherAppWidgetProviderInfo appWidgetInfo =
+ mAppWidgetManager.getLauncherAppWidgetInfo(widgetId);
if (appWidgetInfo != null) {
- mPendingAddWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
- this, appWidgetInfo);
+ mPendingAddWidgetInfo = appWidgetInfo;
mPendingAddInfo.copyFrom(info);
mPendingAddWidgetId = widgetId;
@@ -2676,12 +2681,17 @@
final ShortcutInfo shortcut = (ShortcutInfo) tag;
if (shortcut.isDisabled != 0) {
- int error = R.string.activity_not_available;
- if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SAFEMODE) != 0) {
- error = R.string.safemode_shortcut_error;
+ if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SUSPENDED) != 0
+ || (shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_QUIET_USER) != 0) {
+ // Launch activity anyway, framework will tell the user why the app is suspended.
+ } else {
+ int error = R.string.activity_not_available;
+ if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SAFEMODE) != 0) {
+ error = R.string.safemode_shortcut_error;
+ }
+ Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
+ return;
}
- Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
- return;
}
// Check for abandoned promise
@@ -3588,10 +3598,6 @@
// TODO
}
- protected void disableVoiceButtonProxy(boolean disable) {
- // NO-OP
- }
-
public boolean launcherCallbacksProvidesSearch() {
return (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch());
}
@@ -3997,6 +4003,16 @@
sFolders = folders.clone();
}
+ private void bindSafeModeWidget(LauncherAppWidgetInfo item) {
+ PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item, true);
+ view.updateIcon(mIconCache);
+ item.hostView = view;
+ item.hostView.updateAppWidget(null);
+ item.hostView.setOnClickListener(this);
+ addAppWidgetToWorkspace(item, null, false);
+ mWorkspace.requestLayout();
+ }
+
/**
* Add the views for a widget to the workspace.
*
@@ -4012,19 +4028,31 @@
return;
}
+ if (mIsSafeModeEnabled) {
+ bindSafeModeWidget(item);
+ return;
+ }
+
final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0;
if (DEBUG_WIDGETS) {
Log.d(TAG, "bindAppWidget: " + item);
}
- final Workspace workspace = mWorkspace;
- LauncherAppWidgetProviderInfo appWidgetInfo =
- LauncherModel.getProviderInfo(this, item.providerName, item.user);
+ final LauncherAppWidgetProviderInfo appWidgetInfo;
- if (!mIsSafeModeEnabled
- && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0)
- && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
+ if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
+ // If the provider is not ready, bind as a pending widget.
+ appWidgetInfo = null;
+ } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+ // The widget id is not valid. Try to find the widget based on the provider info.
+ appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user);
+ } else {
+ appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId);
+ }
+ // If the provider is ready, but the width is not yet restored, try to restore it.
+ if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) &&
+ (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
if (appWidgetInfo == null) {
if (DEBUG_WIDGETS) {
Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId
@@ -4036,7 +4064,7 @@
}
// If we do not have a valid id, try to bind an id.
- if ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0) {
+ if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
// Note: This assumes that the id remap broadcast is received before this step.
// If that is not the case, the id remap will be ignored and user may see the
// click to setup view.
@@ -4072,46 +4100,42 @@
: LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
LauncherModel.updateItemInDatabase(this, item);
- } else if (((item.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0)
+ } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
&& (appWidgetInfo.configure == null)) {
- // If the ID is already valid, verify if we need to configure or not.
+ // The widget was marked as UI not ready, but there is no configure activity to
+ // update the UI.
item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
LauncherModel.updateItemInDatabase(this, item);
}
}
- if (!mIsSafeModeEnabled && item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
- final int appWidgetId = item.appWidgetId;
+ if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
if (DEBUG_WIDGETS) {
Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component "
+ appWidgetInfo.provider);
}
// Verify that we own the widget
- AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
- if (info == null || appWidgetInfo == null ||
- !info.provider.equals(appWidgetInfo.provider)) {
- Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId + " info=" + info
- + " appWidgetInfo=" + appWidgetInfo);
+ if (appWidgetInfo == null) {
+ Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
deleteWidgetInfo(item);
return;
}
- item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
+ item.hostView = mAppWidgetHost.createView(this, item.appWidgetId, appWidgetInfo);
item.minSpanX = appWidgetInfo.minSpanX;
item.minSpanY = appWidgetInfo.minSpanY;
+ addAppWidgetToWorkspace(item, appWidgetInfo, false);
} else {
- appWidgetInfo = null;
PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item,
mIsSafeModeEnabled);
view.updateIcon(mIconCache);
item.hostView = view;
item.hostView.updateAppWidget(null);
item.hostView.setOnClickListener(this);
+ addAppWidgetToWorkspace(item, null, false);
}
-
- addAppWidgetToWorkspace(item, appWidgetInfo, false);
- workspace.requestLayout();
+ mWorkspace.requestLayout();
if (DEBUG_WIDGETS) {
Log.d(TAG, "bound widget id="+item.appWidgetId+" in "
@@ -4348,7 +4372,7 @@
}
mWorkspace.removeItemsByComponentName(removedComponents, user);
// Notify the drag controller
- mDragController.onAppsRemoved(new ArrayList<String>(), removedComponents);
+ mDragController.onAppsRemoved(new HashSet<String>(), removedComponents);
}
}
@@ -4372,43 +4396,44 @@
}
/**
- * A package was uninstalled. We take both the super set of packageNames
+ * A package was uninstalled/updated. We take both the super set of packageNames
* in addition to specific applications to remove, the reason being that
* this can be called when a package is updated as well. In that scenario,
- * we only remove specific components from the workspace, where as
+ * we only remove specific components from the workspace and hotseat, where as
* package-removal should clear all items by package name.
- *
- * @param reason if non-zero, the icons are not permanently removed, rather marked as disabled.
- * Implementation of the method from LauncherModel.Callbacks.
*/
@Override
- public void bindComponentsRemoved(final ArrayList<String> packageNames,
- final ArrayList<AppInfo> appInfos, final UserHandleCompat user, final int reason) {
+ public void bindWorkspaceComponentsRemoved(
+ final HashSet<String> packageNames, final HashSet<ComponentName> components,
+ final UserHandleCompat user) {
Runnable r = new Runnable() {
public void run() {
- bindComponentsRemoved(packageNames, appInfos, user, reason);
+ bindWorkspaceComponentsRemoved(packageNames, components, user);
}
};
if (waitUntilResume(r)) {
return;
}
+ if (!packageNames.isEmpty()) {
+ mWorkspace.removeItemsByPackageName(packageNames, user);
+ }
+ if (!components.isEmpty()) {
+ mWorkspace.removeItemsByComponentName(components, user);
+ }
+ // Notify the drag controller
+ mDragController.onAppsRemoved(packageNames, components);
- if (reason == 0) {
- HashSet<ComponentName> removedComponents = new HashSet<ComponentName>();
- for (AppInfo info : appInfos) {
- removedComponents.add(info.componentName);
- }
- if (!packageNames.isEmpty()) {
- mWorkspace.removeItemsByPackageName(packageNames, user);
- }
- if (!removedComponents.isEmpty()) {
- mWorkspace.removeItemsByComponentName(removedComponents, user);
- }
- // Notify the drag controller
- mDragController.onAppsRemoved(packageNames, removedComponents);
+ }
- } else {
- mWorkspace.disableShortcutsByPackageName(packageNames, user, reason);
+ @Override
+ public void bindAppInfosRemoved(final ArrayList<AppInfo> appInfos) {
+ Runnable r = new Runnable() {
+ public void run() {
+ bindAppInfosRemoved(appInfos);
+ }
+ };
+ if (waitUntilResume(r)) {
+ return;
}
// Update AllApps
@@ -4417,15 +4442,15 @@
}
}
- private Runnable mBindPackagesUpdatedRunnable = new Runnable() {
+ private Runnable mBindWidgetModelRunnable = new Runnable() {
public void run() {
- bindAllPackages(mWidgetsModel);
+ bindWidgetsModel(mWidgetsModel);
}
};
@Override
- public void bindAllPackages(final WidgetsModel model) {
- if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) {
+ public void bindWidgetsModel(WidgetsModel model) {
+ if (waitUntilResume(mBindWidgetModelRunnable, true)) {
mWidgetsModel = model;
return;
}
@@ -4436,6 +4461,13 @@
}
}
+ @Override
+ public void notifyWidgetProvidersChanged() {
+ if (mWorkspace.getState().shouldUpdateWidget) {
+ mModel.refreshAndBindWidgetsAndShortcuts(this, mWidgetsView.isEmpty());
+ }
+ }
+
private int mapConfigurationOriActivityInfoOri(int configOri) {
final Display d = getWindowManager().getDefaultDisplay();
int naturalOri = Configuration.ORIENTATION_LANDSCAPE;
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 5271029..e1ade10 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -100,6 +100,8 @@
// For handling managed profiles
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED);
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED);
+ filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_AVAILABLE);
+ filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_UNAVAILABLE);
sContext.registerReceiver(mModel, filter);
UserManagerCompat.getInstance(sContext).enableAndResetCache();
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index b07ccc3..1510558 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -82,12 +82,6 @@
}
}
- @Override
- public void stopListening() {
- super.stopListening();
- clearViews();
- }
-
public void addProviderChangeListener(Runnable callback) {
mProviderChangeListeners.add(callback);
}
@@ -102,6 +96,10 @@
callback.run();
}
}
+
+ if (Utilities.ATLEAST_MARSHMALLOW) {
+ mLauncher.notifyWidgetProvidersChanged();
+ }
}
public AppWidgetHostView createView(Context context, int appWidgetId,
diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java
index 2177f52..1703e41 100644
--- a/src/com/android/launcher3/LauncherBackupAgentHelper.java
+++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java
@@ -25,7 +25,7 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
-import com.android.launcher3.model.MigrateFromRestoreTask;
+import com.android.launcher3.model.GridSizeMigrationTask;
import java.io.IOException;
@@ -118,11 +118,9 @@
LauncherAppState.getLauncherProvider().updateFolderItemsRank();
}
- if (MigrateFromRestoreTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) {
- MigrateFromRestoreTask.markForMigration(getApplicationContext(),
- (int) mHelper.migrationCompatibleProfileData.desktopCols,
- (int) mHelper.migrationCompatibleProfileData.desktopRows,
- mHelper.widgetSizes);
+ if (GridSizeMigrationTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) {
+ GridSizeMigrationTask.markForMigration(getApplicationContext(),
+ mHelper.widgetSizes, mHelper.migrationCompatibleProfileData);
}
LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities();
diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java
index 47d1ce9..cad0f2c 100644
--- a/src/com/android/launcher3/LauncherBackupHelper.java
+++ b/src/com/android/launcher3/LauncherBackupHelper.java
@@ -50,9 +50,10 @@
import com.android.launcher3.backup.nano.BackupProtos.Resource;
import com.android.launcher3.backup.nano.BackupProtos.Screen;
import com.android.launcher3.backup.nano.BackupProtos.Widget;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.model.MigrateFromRestoreTask;
+import com.android.launcher3.model.GridSizeMigrationTask;
import com.android.launcher3.util.Thunk;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import com.google.protobuf.nano.MessageNano;
@@ -315,14 +316,13 @@
return true;
}
- if (MigrateFromRestoreTask.ENABLED &&
- (oldProfile.desktopCols - currentProfile.desktopCols <= 1) &&
- (oldProfile.desktopRows - currentProfile.desktopRows <= 1)) {
- // Allow desktop migration when row and/or column count contracts by 1.
-
+ if (GridSizeMigrationTask.ENABLED) {
+ // One time migrate the workspace when launcher starts.
migrationCompatibleProfileData = initDeviceProfileData(mIdp);
migrationCompatibleProfileData.desktopCols = oldProfile.desktopCols;
migrationCompatibleProfileData.desktopRows = oldProfile.desktopRows;
+ migrationCompatibleProfileData.hotseatCount = oldProfile.hotseatCount;
+ migrationCompatibleProfileData.allappsRank = oldProfile.allappsRank;
return true;
}
return false;
@@ -661,12 +661,14 @@
+ getUserSelectionArg();
Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
where, null, null);
+ AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext);
try {
cursor.moveToPosition(-1);
while(cursor.moveToNext()) {
final long id = cursor.getLong(ID_INDEX);
final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX);
final ComponentName provider = ComponentName.unflattenFromString(providerName);
+
Key key = null;
String backupKey = null;
if (provider != null) {
@@ -685,11 +687,14 @@
} else if (backupKey != null) {
if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount);
if (backupWidgetCount < MAX_WIDGETS_PER_PASS) {
- if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
- UserHandleCompat user = UserHandleCompat.myUserHandle();
- writeRowToBackup(key, packWidget(dpi, provider, user), data);
- mKeys.add(key);
- backupWidgetCount ++;
+ LauncherAppWidgetProviderInfo widgetInfo = widgetManager
+ .getLauncherAppWidgetInfo(cursor.getInt(APPWIDGET_ID_INDEX));
+ if (widgetInfo != null) {
+ if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
+ writeRowToBackup(key, packWidget(dpi, widgetInfo), data);
+ mKeys.add(key);
+ backupWidgetCount ++;
+ }
} else {
if (VERBOSE) Log.v(TAG, "deferring widget backup " + backupKey);
// too many widgets for this pass, request another.
@@ -1005,16 +1010,14 @@
}
/** Serialize a widget for persistence, including a checksum wrapper. */
- private Widget packWidget(int dpi, ComponentName provider, UserHandleCompat user) {
- final LauncherAppWidgetProviderInfo info =
- LauncherModel.getProviderInfo(mContext, provider, user);
+ private Widget packWidget(int dpi, LauncherAppWidgetProviderInfo info) {
Widget widget = new Widget();
- widget.provider = provider.flattenToShortString();
+ widget.provider = info.provider.flattenToShortString();
widget.label = info.label;
widget.configure = info.configure != null;
if (info.icon != 0) {
widget.icon = new Resource();
- Drawable fullResIcon = mIconCache.getFullResIcon(provider.getPackageName(), info.icon);
+ Drawable fullResIcon = mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon);
Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext);
widget.icon.data = Utilities.flattenBitmap(icon);
widget.icon.dpi = dpi;
@@ -1023,7 +1026,6 @@
Point spans = info.getMinSpans(mIdp, mContext);
widget.minSpanX = spans.x;
widget.minSpanY = spans.y;
-
return widget;
}
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index c08cd0b..6ce2293 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -34,7 +34,7 @@
WALLPAPER_CROP_PREFERENCES_KEY + XML,
WALLPAPER_IMAGES_DB,
WIDGET_PREVIEWS_DB,
- MANAGED_USER_PREFERENCES_KEY,
+ MANAGED_USER_PREFERENCES_KEY + XML,
APP_ICONS_DB));
// TODO: Delete these files on upgrade
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 774cca4..c8860e3 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -17,7 +17,6 @@
package com.android.launcher3;
import android.app.SearchManager;
-import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -34,15 +33,12 @@
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
-import android.os.DeadObjectException;
-import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Parcelable;
import android.os.Process;
import android.os.SystemClock;
-import android.os.TransactionTooLargeException;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
@@ -56,12 +52,15 @@
import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.model.MigrateFromRestoreTask;
+import com.android.launcher3.model.GridSizeMigrationTask;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.CursorIconInfo;
+import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.LongArrayMap;
import com.android.launcher3.util.ManagedProfileHeuristic;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.StringFilter;
import com.android.launcher3.util.Thunk;
import java.lang.ref.WeakReference;
@@ -69,7 +68,6 @@
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -99,7 +97,6 @@
private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
private static final long INVALID_SCREEN_ID = -1L;
- @Thunk final boolean mAppsCanBeOnRemoveableStorage;
private final boolean mOldContentProviderExists;
@Thunk final LauncherAppState mApp;
@@ -138,11 +135,9 @@
@Thunk WeakReference<Callbacks> mCallbacks;
// < only access in worker thread >
- AllAppsList mBgAllAppsList;
+ private final AllAppsList mBgAllAppsList;
// Entire list of widgets.
- WidgetsModel mBgWidgetsModel;
- // Keep a clone of widgets that can be accessed from non-worker thread.
- WidgetsModel mFgWidgetsModel;
+ private final WidgetsModel mBgWidgetsModel;
// The lock that must be acquired before referencing any static bg data structures. Unlike
// other locks, this one can generally be held long-term because we never expect any of these
@@ -169,12 +164,6 @@
// sBgWorkspaceScreens is the ordered set of workspace screens.
static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>();
- // sBgWidgetProviders is the set of widget providers including custom internal widgets
- public static HashMap<ComponentKey, LauncherAppWidgetProviderInfo> sBgWidgetProviders;
-
- // sBgShortcutProviders is the set of custom shortcut providers
- public static List<ResolveInfo> sBgShortcutProviders;
-
// sPendingPackages is a set of packages which could be on sdcard and are not available yet
static final HashMap<UserHandleCompat, HashSet<String>> sPendingPackages =
new HashMap<UserHandleCompat, HashSet<String>>();
@@ -207,9 +196,12 @@
ArrayList<ShortcutInfo> removed, UserHandleCompat user);
public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
public void bindRestoreItemsChange(HashSet<ItemInfo> updates);
- public void bindComponentsRemoved(ArrayList<String> packageNames,
- ArrayList<AppInfo> appInfos, UserHandleCompat user, int reason);
- public void bindAllPackages(WidgetsModel model);
+ public void bindWorkspaceComponentsRemoved(
+ HashSet<String> packageNames, HashSet<ComponentName> components,
+ UserHandleCompat user);
+ public void bindAppInfosRemoved(ArrayList<AppInfo> appInfos);
+ public void notifyWidgetProvidersChanged();
+ public void bindWidgetsModel(WidgetsModel model);
public void bindSearchProviderChanged();
public boolean isAllAppsButtonRank(int rank);
public void onPageBoundSynchronously(int page);
@@ -223,7 +215,6 @@
LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
Context context = app.getContext();
- mAppsCanBeOnRemoveableStorage = Environment.isExternalStorageRemovable();
String oldProvider = context.getString(R.string.old_launcher_provider_uri);
// This may be the same as MIGRATE_AUTHORITY, or it may be replaced by a different
// resource string.
@@ -245,7 +236,6 @@
mApp = app;
mBgAllAppsList = new AllAppsList(iconCache, appFilter);
mBgWidgetsModel = new WidgetsModel(context, iconCache, appFilter);
- mFgWidgetsModel = mBgWidgetsModel.clone();
mIconCache = iconCache;
mLauncherApps = LauncherAppsCompat.getInstance(context);
@@ -1238,20 +1228,8 @@
@Override
public void onPackagesAvailable(String[] packageNames, UserHandleCompat user,
boolean replacing) {
- if (!replacing) {
- enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packageNames,
- user));
- if (mAppsCanBeOnRemoveableStorage) {
- // Only rebind if we support removable storage. It catches the
- // case where
- // apps on the external sd card need to be reloaded
- startLoaderFromBackground();
- }
- } else {
- // If we are replacing then just update the packages in the list
- enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE,
- packageNames, user));
- }
+ enqueuePackageUpdated(
+ new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, packageNames, user));
}
@Override
@@ -1264,6 +1242,20 @@
}
}
+ @Override
+ public void onPackagesSuspended(String[] packageNames, UserHandleCompat user) {
+ enqueuePackageUpdated(new PackageUpdatedTask(
+ PackageUpdatedTask.OP_SUSPEND, packageNames,
+ user));
+ }
+
+ @Override
+ public void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user) {
+ enqueuePackageUpdated(new PackageUpdatedTask(
+ PackageUpdatedTask.OP_UNSUSPEND, packageNames,
+ user));
+ }
+
/**
* Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
* ACTION_PACKAGE_CHANGED.
@@ -1285,6 +1277,14 @@
|| LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
UserManagerCompat.getInstance(context).enableAndResetCache();
forceReload();
+ } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) ||
+ LauncherAppsCompat.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
+ UserHandleCompat user = UserHandleCompat.fromIntent(intent);
+ if (user != null) {
+ enqueuePackageUpdated(new PackageUpdatedTask(
+ PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE,
+ new String[0], user));
+ }
}
}
@@ -1736,23 +1736,10 @@
int countX = profile.numColumns;
int countY = profile.numRows;
- if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) {
- long migrationStartTime = System.currentTimeMillis();
- Log.v(TAG, "Starting workspace migration after restore");
- try {
- MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext);
- // Clear the flags before starting the task, so that we do not run the task
- // again, in case there was an uncaught error.
- MigrateFromRestoreTask.clearFlags(mContext);
- task.execute();
- } catch (Exception e) {
- Log.e(TAG, "Error during grid migration", e);
-
- // Clear workspace.
- mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE;
- }
- Log.v(TAG, "Workspace migration completed in "
- + (System.currentTimeMillis() - migrationStartTime));
+ if (GridSizeMigrationTask.ENABLED &&
+ !GridSizeMigrationTask.migrateGridIfNeeded(mContext)) {
+ // Migration failed. Clear workspace.
+ mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE;
}
if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) {
@@ -1776,8 +1763,8 @@
.getInstance(mContext).updateAndGetActiveSessionCache();
sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext));
- final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
- final ArrayList<Long> restoredRows = new ArrayList<Long>();
+ final ArrayList<Long> itemsToRemove = new ArrayList<>();
+ final ArrayList<Long> restoredRows = new ArrayList<>();
final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
final Cursor c = contentResolver.query(contentUri, null, null, null, null);
@@ -1786,6 +1773,7 @@
// Load workspace in reverse order to ensure that latest items are loaded first (and
// before any earlier duplicates)
final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>();
+ HashMap<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
try {
final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
@@ -1822,8 +1810,11 @@
final CursorIconInfo cursorIconInfo = new CursorIconInfo(c);
final LongSparseArray<UserHandleCompat> allUsers = new LongSparseArray<>();
+ final LongSparseArray<Boolean> quietMode = new LongSparseArray<>();
for (UserHandleCompat user : mUserManager.getUserProfiles()) {
- allUsers.put(mUserManager.getSerialNumberForUser(user), user);
+ long serialNo = mUserManager.getSerialNumberForUser(user);
+ allUsers.put(serialNo, user);
+ quietMode.put(serialNo, mUserManager.isQuietModeEnabled(user));
}
ShortcutInfo info;
@@ -1834,6 +1825,7 @@
long serialNumber;
Intent intent;
UserHandleCompat user;
+ String targetPackage;
while (!mStopped && c.moveToNext()) {
try {
@@ -1852,6 +1844,7 @@
int promiseType = c.getInt(restoredIndex);
int disabledState = 0;
boolean itemReplaced = false;
+ targetPackage = null;
if (user == null) {
// User has been deleted remove the item.
itemsToRemove.add(id);
@@ -1865,6 +1858,9 @@
cn.getPackageName(), user);
boolean validComponent = validPkg &&
launcherApps.isActivityEnabledForProfile(cn, user);
+ if (validPkg) {
+ targetPackage = cn.getPackageName();
+ }
if (validComponent) {
if (restored) {
@@ -1872,6 +1868,9 @@
restoredRows.add(id);
restored = false;
}
+ if (quietMode.get(serialNumber)) {
+ disabledState = ShortcutInfo.FLAG_DISABLED_QUIET_USER;
+ }
} else if (validPkg) {
intent = null;
if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) {
@@ -1940,9 +1939,8 @@
itemsToRemove.add(id);
continue;
}
- } else if (launcherApps.isAppEnabled(
- manager, cn.getPackageName(),
- PackageManager.GET_UNINSTALLED_PACKAGES)) {
+ } else if (PackageManagerHelper.isAppOnSdcard(
+ manager, cn.getPackageName())) {
// Package is present but not available.
allowMissingTarget = true;
disabledState = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
@@ -1985,7 +1983,7 @@
if (itemReplaced) {
if (user.equals(UserHandleCompat.myUserHandle())) {
- info = getAppShortcutInfo(manager, intent, user, context, null,
+ info = getAppShortcutInfo(intent, user, context, null,
cursorIconInfo.iconIndex, titleIndex,
false, useLowResIcon);
} else {
@@ -2008,12 +2006,17 @@
}
} else if (itemType ==
LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
- info = getAppShortcutInfo(manager, intent, user, context, c,
+ info = getAppShortcutInfo(intent, user, context, c,
cursorIconInfo.iconIndex, titleIndex,
allowMissingTarget, useLowResIcon);
} else {
info = getShortcutInfo(c, context, titleIndex, cursorIconInfo);
+ // Shortcuts are only available on the primary profile
+ if (PackageManagerHelper.isAppSuspended(manager, targetPackage)) {
+ disabledState |= ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+ }
+
// App shortcuts that used to be automatically added to Launcher
// didn't always have the correct intent flags set, so do that
// here
@@ -2041,7 +2044,7 @@
if (info.promisedIntent != null) {
info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber);
}
- info.isDisabled = disabledState;
+ info.isDisabled |= disabledState;
if (isSafeMode && !Utilities.isSystemApp(context, intent)) {
info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE;
}
@@ -2144,10 +2147,14 @@
final boolean wasProviderReady = (restoreStatus &
LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0;
- final LauncherAppWidgetProviderInfo provider =
- LauncherModel.getProviderInfo(context,
+ if (widgetProvidersMap == null) {
+ widgetProvidersMap = AppWidgetManagerCompat
+ .getInstance(mContext).getAllProvidersMap();
+ }
+ final AppWidgetProviderInfo provider = widgetProvidersMap.get(
+ new ComponentKey(
ComponentName.unflattenFromString(savedProvider),
- user);
+ user));
final boolean isProviderReady = isValidProvider(provider);
if (!isSafeMode && !customWidget &&
@@ -2766,7 +2773,6 @@
final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
if (callbacks != null) {
callbacks.bindAllApplications(list);
- callbacks.bindAllPackages(mFgWidgetsModel);
}
if (DEBUG_LOADERS) {
Log.d(TAG, "bound all " + list.size() + " apps from cache in "
@@ -2810,12 +2816,12 @@
if (apps == null || apps.isEmpty()) {
return;
}
-
+ boolean quietMode = mUserManager.isQuietModeEnabled(user);
// Create the ApplicationInfos
for (int i = 0; i < apps.size(); i++) {
LauncherActivityInfoCompat app = apps.get(i);
// This builds the icon bitmaps.
- mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache));
+ mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, quietMode));
}
final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user);
@@ -2858,7 +2864,7 @@
callbacks.bindAllApplications(added);
if (DEBUG_LOADERS) {
Log.d(TAG, "bound " + added.size() + " apps in "
- + (SystemClock.uptimeMillis() - bindTime) + "ms");
+ + (SystemClock.uptimeMillis() - bindTime) + "ms");
}
} else {
Log.i(TAG, "not binding apps: no Launcher activity");
@@ -2867,8 +2873,6 @@
});
// Cleanup any data stored for a deleted user.
ManagedProfileHeuristic.processAllUsers(profiles, mContext);
-
- loadAndBindWidgetsAndShortcuts(tryGetCallbacks(oldCallbacks), true /* refresh */);
if (DEBUG_LOADERS) {
Log.d(TAG, "Icons processed in "
+ (SystemClock.uptimeMillis() - loadTime) + "ms");
@@ -2935,9 +2939,6 @@
}
});
}
-
- // Reload widget list. No need to refresh, as we only want to update the icons and labels.
- loadAndBindWidgetsAndShortcuts(callbacks, false);
}
void enqueuePackageUpdated(PackageUpdatedTask task) {
@@ -2960,10 +2961,7 @@
packagesUnavailable.clear();
for (String pkg : entry.getValue()) {
if (!launcherApps.isPackageEnabledForProfile(pkg, user)) {
- boolean packageOnSdcard = launcherApps.isAppEnabled(
- manager, pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
- if (packageOnSdcard) {
- Launcher.addDumpLog(TAG, "Package found on sd-card: " + pkg, true);
+ if (PackageManagerHelper.isAppOnSdcard(manager, pkg)) {
packagesUnavailable.add(pkg);
} else {
Launcher.addDumpLog(TAG, "Package not found: " + pkg, true);
@@ -2995,7 +2993,9 @@
public static final int OP_UPDATE = 2;
public static final int OP_REMOVE = 3; // uninstlled
public static final int OP_UNAVAILABLE = 4; // external media unmounted
-
+ public static final int OP_SUSPEND = 5; // package suspended
+ public static final int OP_UNSUSPEND = 6; // package unsuspended
+ public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable
public PackageUpdatedTask(int op, String[] packages, UserHandleCompat user) {
mOp = op;
@@ -3012,6 +3012,8 @@
final String[] packages = mPackages;
final int N = packages.length;
+ FlagOp flagOp = FlagOp.NO_OP;
+ StringFilter pkgFilter = StringFilter.of(new HashSet<>(Arrays.asList(packages)));
switch (mOp) {
case OP_ADD: {
for (int i=0; i<N; i++) {
@@ -3033,6 +3035,8 @@
mBgAllAppsList.updatePackage(context, packages[i], mUser);
mApp.getWidgetCache().removePackage(packages[i], mUser);
}
+ // Since package was just updated, the target must be available now.
+ flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
case OP_REMOVE: {
ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
@@ -3051,6 +3055,23 @@
mBgAllAppsList.removePackage(packages[i], mUser);
mApp.getWidgetCache().removePackage(packages[i], mUser);
}
+ flagOp = FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
+ break;
+ case OP_SUSPEND:
+ case OP_UNSUSPEND:
+ flagOp = mOp == OP_SUSPEND ?
+ FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED) :
+ FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED);
+ if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
+ mBgAllAppsList.updatePackageFlags(pkgFilter, mUser, flagOp);
+ break;
+ case OP_USER_AVAILABILITY_CHANGE:
+ flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
+ ? FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER)
+ : FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER);
+ // We want to update all packages for this user.
+ pkgFilter = StringFilter.matchesAll();
+ mBgAllAppsList.updatePackageFlags(pkgFilter, mUser, flagOp);
break;
}
@@ -3059,11 +3080,11 @@
final ArrayList<AppInfo> removedApps = new ArrayList<AppInfo>();
if (mBgAllAppsList.added.size() > 0) {
- added = new ArrayList<AppInfo>(mBgAllAppsList.added);
+ added = new ArrayList<>(mBgAllAppsList.added);
mBgAllAppsList.added.clear();
}
if (mBgAllAppsList.modified.size() > 0) {
- modified = new ArrayList<AppInfo>(mBgAllAppsList.modified);
+ modified = new ArrayList<>(mBgAllAppsList.modified);
mBgAllAppsList.modified.clear();
}
if (mBgAllAppsList.removed.size() > 0) {
@@ -3071,14 +3092,7 @@
mBgAllAppsList.removed.clear();
}
- final Callbacks callbacks = getCallback();
- if (callbacks == null) {
- Log.w(TAG, "Nobody to tell about the new app. Launcher is probably loading.");
- return;
- }
-
- final HashMap<ComponentName, AppInfo> addedOrUpdatedApps =
- new HashMap<ComponentName, AppInfo>();
+ final HashMap<ComponentName, AppInfo> addedOrUpdatedApps = new HashMap<>();
if (added != null) {
addAppsToAllApps(context, added);
@@ -3088,6 +3102,7 @@
}
if (modified != null) {
+ final Callbacks callbacks = getCallback();
final ArrayList<AppInfo> modifiedFinal = modified;
for (AppInfo ai : modified) {
addedOrUpdatedApps.put(ai.componentName, ai);
@@ -3104,12 +3119,11 @@
}
// Update shortcut infos
- if (mOp == OP_ADD || mOp == OP_UPDATE) {
+ if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<ShortcutInfo>();
final ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<ShortcutInfo>();
final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<LauncherAppWidgetInfo>();
- HashSet<String> packageSet = new HashSet<String>(Arrays.asList(packages));
synchronized (sBgLock) {
for (ItemInfo info : sBgItemsIdMap) {
if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
@@ -3119,7 +3133,7 @@
// Update shortcuts which use iconResource.
if ((si.iconResource != null)
- && packageSet.contains(si.iconResource.packageName)) {
+ && pkgFilter.matches(si.iconResource.packageName)) {
Bitmap icon = Utilities.createIconBitmap(
si.iconResource.packageName,
si.iconResource.resourceName, context);
@@ -3131,7 +3145,7 @@
}
ComponentName cn = si.getTargetComponent();
- if (cn != null && packageSet.contains(cn.getPackageName())) {
+ if (cn != null && pkgFilter.matches(cn.getPackageName())) {
AppInfo appInfo = addedOrUpdatedApps.get(cn);
if (si.isPromise()) {
@@ -3179,9 +3193,9 @@
infoUpdated = true;
}
- if ((si.isDisabled & ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE) != 0) {
- // Since package was just updated, the target must be available now.
- si.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
+ int oldDisabledFlags = si.isDisabled;
+ si.isDisabled = flagOp.apply(si.isDisabled);
+ if (si.isDisabled != oldDisabledFlags) {
shortcutUpdated = true;
}
}
@@ -3192,11 +3206,11 @@
if (infoUpdated) {
updateItemInDatabase(context, si);
}
- } else if (info instanceof LauncherAppWidgetInfo) {
+ } else if (info instanceof LauncherAppWidgetInfo && mOp == OP_ADD) {
LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
if (mUser.equals(widgetInfo.user)
&& widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
- && packageSet.contains(widgetInfo.providerName.getPackageName())) {
+ && pkgFilter.matches(widgetInfo.providerName.getPackageName())) {
widgetInfo.restoreStatus &=
~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
@@ -3214,6 +3228,7 @@
}
if (!updatedShortcuts.isEmpty() || !removedShortcuts.isEmpty()) {
+ final Callbacks callbacks = getCallback();
mHandler.post(new Runnable() {
public void run() {
@@ -3229,6 +3244,7 @@
}
}
if (!widgets.isEmpty()) {
+ final Callbacks callbacks = getCallback();
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = getCallback();
@@ -3240,222 +3256,109 @@
}
}
- final ArrayList<String> removedPackageNames =
- new ArrayList<String>();
- if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE) {
+ final HashSet<String> removedPackages = new HashSet<>();
+ final HashSet<ComponentName> removedComponents = new HashSet<>();
+ if (mOp == OP_REMOVE) {
// Mark all packages in the broadcast to be removed
- removedPackageNames.addAll(Arrays.asList(packages));
+ Collections.addAll(removedPackages, packages);
+
+ // No need to update the removedComponents as
+ // removedPackages is a super-set of removedComponents
} else if (mOp == OP_UPDATE) {
// Mark disabled packages in the broadcast to be removed
for (int i=0; i<N; i++) {
if (isPackageDisabled(context, packages[i], mUser)) {
- removedPackageNames.add(packages[i]);
+ removedPackages.add(packages[i]);
}
}
+
+ // Update removedComponents as some components can get removed during package update
+ for (AppInfo info : removedApps) {
+ removedComponents.add(info.componentName);
+ }
}
- if (!removedPackageNames.isEmpty() || !removedApps.isEmpty()) {
- final int removeReason;
- if (mOp == OP_UNAVAILABLE) {
- removeReason = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
- } else {
- // Remove all the components associated with this package
- for (String pn : removedPackageNames) {
- deletePackageFromDatabase(context, pn, mUser);
- }
- // Remove all the specific components
- for (AppInfo a : removedApps) {
- ArrayList<ItemInfo> infos = getItemInfoForComponentName(a.componentName, mUser);
- deleteItemsFromDatabase(context, infos);
- }
- removeReason = 0;
+ if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
+ for (String pn : removedPackages) {
+ deletePackageFromDatabase(context, pn, mUser);
+ }
+ for (ComponentName cn : removedComponents) {
+ deleteItemsFromDatabase(context, getItemInfoForComponentName(cn, mUser));
}
// Remove any queued items from the install queue
- InstallShortcutReceiver.removeFromInstallQueue(context, removedPackageNames, mUser);
+ InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
+
// Call the components-removed callback
+ final Callbacks callbacks = getCallback();
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = getCallback();
if (callbacks == cb && cb != null) {
- callbacks.bindComponentsRemoved(
- removedPackageNames, removedApps, mUser, removeReason);
+ callbacks.bindWorkspaceComponentsRemoved(
+ removedPackages, removedComponents, mUser);
}
}
});
}
- // Update widgets
- if (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE) {
- // Always refresh for a package event on secondary user
- boolean needToRefresh = !mUser.equals(UserHandleCompat.myUserHandle());
-
- // Refresh widget list, if the package already had a widget.
- synchronized (sBgLock) {
- if (sBgWidgetProviders != null) {
- HashSet<String> pkgSet = new HashSet<>();
- Collections.addAll(pkgSet, mPackages);
-
- for (ComponentKey key : sBgWidgetProviders.keySet()) {
- needToRefresh |= key.user.equals(mUser) &&
- pkgSet.contains(key.componentName.getPackageName());
+ if (!removedApps.isEmpty()) {
+ // Remove corresponding apps from All-Apps
+ final Callbacks callbacks = getCallback();
+ mHandler.post(new Runnable() {
+ public void run() {
+ Callbacks cb = getCallback();
+ if (callbacks == cb && cb != null) {
+ callbacks.bindAppInfosRemoved(removedApps);
}
}
- }
+ });
+ }
- if (!needToRefresh && mOp != OP_REMOVE) {
- // Refresh widget list, if there is any newly added widget
- PackageManager pm = context.getPackageManager();
- for (String pkg : mPackages) {
- try {
- List<ResolveInfo> widgets = pm.queryBroadcastReceivers(
- new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
- .setPackage(pkg), 0);
- needToRefresh |= widgets != null && !widgets.isEmpty();
- } catch (RuntimeException e) {
- if (LauncherAppState.isDogfoodBuild()) {
- throw e;
- }
- // Ignore the crash. We can live with a state widget list.
- Log.e(TAG, "PM call failed for " + pkg, e);
+ // Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to
+ // get widget update signals.
+ if (!Utilities.ATLEAST_MARSHMALLOW &&
+ (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) {
+ final Callbacks callbacks = getCallback();
+ mHandler.post(new Runnable() {
+ public void run() {
+ Callbacks cb = getCallback();
+ if (callbacks == cb && cb != null) {
+ callbacks.notifyWidgetProvidersChanged();
}
}
- }
-
- loadAndBindWidgetsAndShortcuts(callbacks, needToRefresh);
- }
-
- // Write all the logs to disk
- mHandler.post(new Runnable() {
- public void run() {
- Callbacks cb = getCallback();
- if (callbacks == cb && cb != null) {
- callbacks.dumpLogsToLocalData();
- }
- }
- });
- }
- }
-
- public static List<LauncherAppWidgetProviderInfo> getWidgetProviders(Context context,
- boolean refresh) {
- ArrayList<LauncherAppWidgetProviderInfo> results =
- new ArrayList<LauncherAppWidgetProviderInfo>();
- try {
- synchronized (sBgLock) {
- if (sBgWidgetProviders == null || refresh) {
- HashMap<ComponentKey, LauncherAppWidgetProviderInfo> tmpWidgetProviders
- = new HashMap<>();
- AppWidgetManagerCompat wm = AppWidgetManagerCompat.getInstance(context);
- LauncherAppWidgetProviderInfo info;
-
- List<AppWidgetProviderInfo> widgets = wm.getAllProviders();
- for (AppWidgetProviderInfo pInfo : widgets) {
- info = LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo);
- UserHandleCompat user = wm.getUser(info);
- tmpWidgetProviders.put(new ComponentKey(info.provider, user), info);
- }
-
- Collection<CustomAppWidget> customWidgets = Launcher.getCustomAppWidgets().values();
- for (CustomAppWidget widget : customWidgets) {
- info = new LauncherAppWidgetProviderInfo(context, widget);
- UserHandleCompat user = wm.getUser(info);
- tmpWidgetProviders.put(new ComponentKey(info.provider, user), info);
- }
- // Replace the global list at the very end, so that if there is an exception,
- // previously loaded provider list is used.
- sBgWidgetProviders = tmpWidgetProviders;
- }
- results.addAll(sBgWidgetProviders.values());
- return results;
- }
- } catch (Exception e) {
- if (!LauncherAppState.isDogfoodBuild() &&
- (e.getCause() instanceof TransactionTooLargeException ||
- e.getCause() instanceof DeadObjectException)) {
- // the returned value may be incomplete and will not be refreshed until the next
- // time Launcher starts.
- // TODO: after figuring out a repro step, introduce a dirty bit to check when
- // onResume is called to refresh the widget provider list.
- synchronized (sBgLock) {
- if (sBgWidgetProviders != null) {
- results.addAll(sBgWidgetProviders.values());
- }
- return results;
- }
- } else {
- throw e;
+ });
}
}
}
- public static LauncherAppWidgetProviderInfo getProviderInfo(Context ctx, ComponentName name,
- UserHandleCompat user) {
- synchronized (sBgLock) {
- if (sBgWidgetProviders == null) {
- getWidgetProviders(ctx, false /* refresh */);
- }
- return sBgWidgetProviders.get(new ComponentKey(name, user));
- }
- }
-
- public void loadAndBindWidgetsAndShortcuts(final Callbacks callbacks, final boolean refresh) {
-
- runOnWorkerThread(new Runnable() {
+ private void bindWidgetsModel(final Callbacks callbacks, final WidgetsModel model) {
+ mHandler.post(new Runnable() {
@Override
public void run() {
- updateWidgetsModel(refresh);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- Callbacks cb = getCallback();
- if (callbacks == cb && cb != null) {
- callbacks.bindAllPackages(mFgWidgetsModel);
- }
- }
- });
- // update the Widget entries inside DB on the worker thread.
- LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews(
- mFgWidgetsModel.getRawList());
+ Callbacks cb = getCallback();
+ if (callbacks == cb && cb != null) {
+ callbacks.bindWidgetsModel(model);
+ }
}
});
}
- /**
- * Returns a list of ResolveInfos/AppWidgetInfos.
- *
- * @see #loadAndBindWidgetsAndShortcuts
- */
- @Thunk void updateWidgetsModel(boolean refresh) {
- Utilities.assertWorkerThread();
- PackageManager packageManager = mApp.getContext().getPackageManager();
- final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
- widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh));
-
- // Update shortcut providers
- synchronized (sBgLock) {
- try {
- Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
- List<ResolveInfo> providers = packageManager.queryIntentActivities(shortcutsIntent, 0);
- sBgShortcutProviders = providers;
- } catch (RuntimeException e) {
- if (!LauncherAppState.isDogfoodBuild() &&
- (e.getCause() instanceof TransactionTooLargeException ||
- e.getCause() instanceof DeadObjectException)) {
- /**
- * Ignore exception and use the cached list if available.
- * Refer to {@link #getWidgetProviders(Context, boolean}} for more info.
- */
- } else {
- throw e;
+ public void refreshAndBindWidgetsAndShortcuts(
+ final Callbacks callbacks, final boolean bindFirst) {
+ runOnWorkerThread(new Runnable() {
+ @Override
+ public void run() {
+ if (bindFirst && !mBgWidgetsModel.isEmpty()) {
+ bindWidgetsModel(callbacks, mBgWidgetsModel.clone());
}
+ final WidgetsModel model = mBgWidgetsModel.updateAndClone(mApp.getContext());
+ bindWidgetsModel(callbacks, model);
+ // update the Widget entries inside DB on the worker thread.
+ LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews(
+ model.getRawList());
}
- if (sBgShortcutProviders != null) {
- widgetsAndShortcuts.addAll(sBgShortcutProviders);
- }
- }
- mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts);
- mFgWidgetsModel = mBgWidgetsModel.clone();
+ });
}
@Thunk static boolean isPackageDisabled(Context context, String packageName,
@@ -3545,7 +3448,7 @@
*
* If c is not null, then it will be used to fill in missing data like the title and icon.
*/
- public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent,
+ public ShortcutInfo getAppShortcutInfo(Intent intent,
UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex,
boolean allowMissingTarget, boolean useLowResIcon) {
if (user == null) {
@@ -3575,6 +3478,10 @@
info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon);
}
+ if (lai != null && PackageManagerHelper.isAppSuspended(lai.getApplicationInfo())) {
+ info.isDisabled = ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+ }
+
// from the db
if (TextUtils.isEmpty(info.title) && c != null) {
info.title = Utilities.trim(c.getString(titleIndex));
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 4760930..7c4b78d 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -78,7 +78,7 @@
private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
@Thunk LauncherProviderChangeListener mListener;
- @Thunk DatabaseHelper mOpenHelper;
+ protected DatabaseHelper mOpenHelper;
@Override
public boolean onCreate() {
@@ -113,6 +113,15 @@
}
}
+ /**
+ * Overridden in tests
+ */
+ protected synchronized void createDbIfNotExists() {
+ if (mOpenHelper == null) {
+ mOpenHelper = new DatabaseHelper(getContext());
+ }
+ }
+
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
@@ -259,9 +268,14 @@
switch (method) {
case LauncherSettings.Settings.METHOD_GET_BOOLEAN: {
Bundle result = new Bundle();
- result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
- Utilities.getPrefs(getContext()).getBoolean(arg, extras.getBoolean(
- LauncherSettings.Settings.EXTRA_DEFAULT_VALUE)));
+ if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(arg)) {
+ result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
+ Utilities.isAllowRotationPrefEnabled(getContext()));
+ } else {
+ result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
+ Utilities.getPrefs(getContext()).getBoolean(arg, extras.getBoolean(
+ LauncherSettings.Settings.EXTRA_DEFAULT_VALUE)));
+ }
return result;
}
case LauncherSettings.Settings.METHOD_SET_BOOLEAN: {
@@ -317,7 +331,10 @@
return folderIds;
}
- private void notifyListeners() {
+ /**
+ * Overridden in tests
+ */
+ protected void notifyListeners() {
// always notify the backup agent
LauncherBackupAgentHelper.dataChanged(getContext());
if (mListener != null) {
@@ -459,7 +476,10 @@
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
}
- private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
+ /**
+ * The class is subclassed in tests to create an in-memory db.
+ */
+ protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
private final Context mContext;
@Thunk final AppWidgetHost mAppWidgetHost;
private long mMaxItemId = -1;
@@ -494,6 +514,18 @@
}
}
+ /**
+ * Constructor used only in tests.
+ */
+ public DatabaseHelper(Context context, String tableName) {
+ super(context, tableName, null, DATABASE_VERSION);
+ mContext = context;
+
+ mAppWidgetHost = null;
+ mMaxItemId = initializeMaxItemId(getWritableDatabase());
+ mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
+ }
+
private boolean tableExists(String tableName) {
Cursor c = getReadableDatabase().query(
true, "sqlite_master", new String[] {"tbl_name"},
@@ -544,18 +576,28 @@
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
- setFlagEmptyDbCreated();
+ onEmptyDbCreated();
+ }
+
+ /**
+ * Overriden in tests.
+ */
+ protected void onEmptyDbCreated() {
+ // Set the flag for empty DB
+ Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
// When a new DB is created, remove all previously stored managed profile information.
- ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext);
+ ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(),
+ mContext);
+ }
+
+ protected long getDefaultUserSerial() {
+ return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
+ UserHandleCompat.myUserHandle());
}
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
- UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
- long userSerialNumber = userManager.getSerialNumberForUser(
- UserHandleCompat.myUserHandle());
String ifNotExists = optional ? " IF NOT EXISTS " : "";
-
db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" +
"_id INTEGER PRIMARY KEY," +
"title TEXT," +
@@ -578,7 +620,7 @@
"appWidgetProvider TEXT," +
"modified INTEGER NOT NULL DEFAULT 0," +
"restored INTEGER NOT NULL DEFAULT 0," +
- "profileId INTEGER DEFAULT " + userSerialNumber + "," +
+ "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," +
"rank INTEGER NOT NULL DEFAULT 0," +
"options INTEGER NOT NULL DEFAULT 0" +
");");
@@ -628,10 +670,6 @@
Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
}
- private void setFlagEmptyDbCreated() {
- Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
- }
-
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 01d670d..55e6395 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -113,7 +113,7 @@
/**
* The content:// style URL for this table
*/
- static final Uri CONTENT_URI = Uri.parse("content://" +
+ public static final Uri CONTENT_URI = Uri.parse("content://" +
ProviderConfig.AUTHORITY + "/" + TABLE_NAME);
/**
diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
index 30cae31..b95e2b0 100644
--- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java
+++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
@@ -702,7 +702,7 @@
private void startWorkspaceSearchBarAnimation(
final Workspace.State toWorkspaceState, int duration, AnimatorSet animation) {
final SearchDropTargetBar.State toSearchBarState =
- toWorkspaceState.getSearchDropTargetBarState();
+ toWorkspaceState.searchDropTargetBarState;
mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration, animation);
}
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index 6bdcb4b..60e080e 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -113,6 +113,16 @@
public static final int FLAG_DISABLED_NOT_AVAILABLE = 2;
/**
+ * Indicates that the icon is disabled as the app is suspended
+ */
+ public static final int FLAG_DISABLED_SUSPENDED = 4;
+
+ /**
+ * Indicates that the icon is disabled as the user is in quiet mode.
+ */
+ public static final int FLAG_DISABLED_QUIET_USER = 8;
+
+ /**
* Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when
* sd-card is not available).
*/
@@ -178,6 +188,7 @@
intent = new Intent(info.intent);
customIcon = false;
flags = info.flags;
+ isDisabled = info.isDisabled;
}
public void setIcon(Bitmap b) {
@@ -288,5 +299,10 @@
shortcut.flags = AppInfo.initFlags(info);
return shortcut;
}
+
+ @Override
+ public boolean isDisabled() {
+ return isDisabled != 0;
+ }
}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 87c9262..271e581 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -117,6 +117,18 @@
public static final boolean ATLEAST_JB_MR2 =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ public static boolean isNycOrAbove() {
+ // TODO: Replace using reflection with looking at the API version once
+ // Build.VERSION.SDK_INT gets bumped to 24. b/22942492.
+ try {
+ View.class.getDeclaredField("DRAG_FLAG_OPAQUE");
+ // View.DRAG_FLAG_OPAQUE doesn't exist in M-release, so it's an indication of N+.
+ return true;
+ } catch (NoSuchFieldException e) {
+ return false;
+ }
+ }
+
// These values are same as that in {@link AsyncTask}.
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
@@ -129,27 +141,30 @@
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
- // To turn on these properties, type
- // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS]
- private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate";
- private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY);
-
public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
public static boolean isPropertyEnabled(String propertyName) {
return Log.isLoggable(propertyName, Log.VERBOSE);
}
- public static boolean isAllowRotationPrefEnabled(Context context, boolean multiProcess) {
- SharedPreferences sharedPrefs = context.getSharedPreferences(
- LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE | (multiProcess ?
- Context.MODE_MULTI_PROCESS : 0));
- boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false);
- return sForceEnableRotation || allowRotationPref;
- }
-
- public static boolean isRotationAllowedForDevice(Context context) {
- return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation);
+ public static boolean isAllowRotationPrefEnabled(Context context) {
+ boolean allowRotationPref = false;
+ if (isNycOrAbove()) {
+ // If the device was scaled, used the original dimensions to determine if rotation
+ // is allowed of not.
+ try {
+ // TODO: Use the actual field when the API is finalized.
+ int originalDensity =
+ DisplayMetrics.class.getField("DENSITY_DEVICE_STABLE").getInt(null);
+ Resources res = context.getResources();
+ int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
+ * res.getDisplayMetrics().densityDpi / originalDensity;
+ allowRotationPref = originalSmallestWidth >= 600;
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ return getPrefs(context).getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, allowRotationPref);
}
public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 7b873d4..86c25b7 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -68,6 +68,7 @@
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate;
import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.util.LongArrayMap;
import com.android.launcher3.util.Thunk;
@@ -180,22 +181,20 @@
// in all apps or customize mode)
enum State {
- NORMAL (SearchDropTargetBar.State.SEARCH_BAR),
- NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE_TRANSLATED),
- SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET),
- OVERVIEW (SearchDropTargetBar.State.INVISIBLE),
- OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE);
+ NORMAL (SearchDropTargetBar.State.SEARCH_BAR, false),
+ NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE_TRANSLATED, false),
+ SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET, false),
+ OVERVIEW (SearchDropTargetBar.State.INVISIBLE, true),
+ OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE, true);
- private final SearchDropTargetBar.State mBarState;
+ public final SearchDropTargetBar.State searchDropTargetBarState;
+ public final boolean shouldUpdateWidget;
- State(SearchDropTargetBar.State searchBarState) {
- mBarState = searchBarState;
+ State(SearchDropTargetBar.State searchBarState, boolean shouldUpdateWidget) {
+ searchDropTargetBarState = searchBarState;
+ this.shouldUpdateWidget = shouldUpdateWidget;
}
-
- public SearchDropTargetBar.State getSearchDropTargetBarState() {
- return mBarState;
- }
- };
+ }
private State mState = State.NORMAL;
private boolean mIsSwitchingState = false;
@@ -2017,10 +2016,16 @@
Animator workspaceAnim = mStateTransitionAnimation.getAnimationToState(mState,
toState, toPage, animated, layerViews);
+ boolean shouldNotifyWidgetChange = !mState.shouldUpdateWidget
+ && toState.shouldUpdateWidget;
// Update the current state
mState = toState;
updateAccessibilityFlags();
+ if (shouldNotifyWidgetChange) {
+ mLauncher.notifyWidgetProvidersChanged();
+ }
+
return workspaceAnim;
}
@@ -4160,41 +4165,10 @@
});
}
- public void disableShortcutsByPackageName(final ArrayList<String> packages,
- final UserHandleCompat user, final int reason) {
- final HashSet<String> packageNames = new HashSet<String>();
- packageNames.addAll(packages);
-
- mapOverItems(MAP_RECURSE, new ItemOperator() {
- @Override
- public boolean evaluate(ItemInfo info, View v, View parent) {
- if (info instanceof ShortcutInfo && v instanceof BubbleTextView) {
- ShortcutInfo shortcutInfo = (ShortcutInfo) info;
- ComponentName cn = shortcutInfo.getTargetComponent();
- if (user.equals(shortcutInfo.user) && cn != null
- && packageNames.contains(cn.getPackageName())) {
- shortcutInfo.isDisabled |= reason;
- BubbleTextView shortcut = (BubbleTextView) v;
- shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache);
-
- if (parent != null) {
- parent.invalidate();
- }
- }
- }
- // process all the shortcuts
- return false;
- }
- });
- }
-
// Removes ALL items that match a given package name, this is usually called when a package
// has been removed and we want to remove all components (widgets, shortcuts, apps) that
// belong to that package.
- void removeItemsByPackageName(final ArrayList<String> packages, final UserHandleCompat user) {
- final HashSet<String> packageNames = new HashSet<String>();
- packageNames.addAll(packages);
-
+ void removeItemsByPackageName(final HashSet<String> packageNames, final UserHandleCompat user) {
// Filter out all the ItemInfos that this is going to affect
final HashSet<ItemInfo> infos = new HashSet<ItemInfo>();
final HashSet<ComponentName> cns = new HashSet<ComponentName>();
@@ -4376,7 +4350,7 @@
}
public void removeAbandonedPromise(String packageName, UserHandleCompat user) {
- ArrayList<String> packages = new ArrayList<String>(1);
+ HashSet<String> packages = new HashSet<>(1);
packages.add(packageName);
LauncherModel.deletePackageFromDatabase(mLauncher, packageName, user);
removeItemsByPackageName(packages, user);
@@ -4400,13 +4374,22 @@
});
}
- void widgetsRestored(ArrayList<LauncherAppWidgetInfo> changedInfo) {
+ public void widgetsRestored(ArrayList<LauncherAppWidgetInfo> changedInfo) {
if (!changedInfo.isEmpty()) {
DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo,
mLauncher.getAppWidgetHost());
- if (LauncherModel.getProviderInfo(getContext(),
- changedInfo.get(0).providerName,
- changedInfo.get(0).user) != null) {
+
+ LauncherAppWidgetInfo item = changedInfo.get(0);
+ final AppWidgetProviderInfo widgetInfo;
+ if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+ widgetInfo = AppWidgetManagerCompat
+ .getInstance(mLauncher).findProvider(item.providerName, item.user);
+ } else {
+ widgetInfo = AppWidgetManagerCompat.getInstance(mLauncher)
+ .getAppWidgetInfo(item.appWidgetId);
+ }
+
+ if (widgetInfo != null) {
// Re-inflate the widgets which have changed status
widgetRefresh.run();
} else {
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index d41224a..0460c91 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -24,12 +24,16 @@
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.net.Uri;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Recycler;
+import android.support.v7.widget.RecyclerView.State;
+import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -40,10 +44,8 @@
import com.android.launcher3.AppInfo;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Thunk;
import java.util.HashMap;
import java.util.List;
@@ -103,18 +105,67 @@
// adapter views
final AccessibilityRecordCompat record = AccessibilityEventCompat
.asRecord(event);
+
+ // count the number of SECTION_BREAK_VIEW_TYPE that is wrongfully
+ // initialized as a node (also a row) for talk back.
+ int numEmptyNode = getEmptyRowForAccessibility(-1 /* no view type */);
+ record.setFromIndex(event.getFromIndex() - numEmptyNode);
+ record.setToIndex(event.getToIndex() - numEmptyNode);
record.setItemCount(mApps.getNumFilteredApps());
}
@Override
+ public void onInitializeAccessibilityNodeInfoForItem(Recycler recycler,
+ State state, View host, AccessibilityNodeInfoCompat info) {
+
+ int viewType = getItemViewType(host);
+ // Only initialize on node that is meaningful. Subtract empty row count.
+ if (viewType == ICON_VIEW_TYPE || viewType == PREDICTION_ICON_VIEW_TYPE) {
+ super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
+ CollectionItemInfoCompat itemInfo = info.getCollectionItemInfo();
+ final CollectionItemInfoCompat dstItemInfo = CollectionItemInfoCompat.obtain(
+ itemInfo.getRowIndex() - getEmptyRowForAccessibility(viewType),
+ itemInfo.getRowSpan(),
+ itemInfo.getColumnIndex(),
+ itemInfo.getColumnSpan(),
+ itemInfo.isHeading(),
+ itemInfo.isSelected());
+ info.setCollectionItemInfo(dstItemInfo);
+ }
+ }
+
+ @Override
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
RecyclerView.State state) {
- if (mApps.hasNoFilteredResults()) {
- // Disregard the no-search-results text as a list item for accessibility
- return 0;
+ return super.getRowCountForAccessibility(recycler, state)
+ - getEmptyRowForAccessibility(-1 /* no view type */);
+ }
+
+ /**
+ * Returns the total number of SECTION_BREAK_VIEW_TYPE that is wrongfully
+ * initialized as a node (also a row) for talk back.
+ */
+ private int getEmptyRowForAccessibility(int viewType) {
+ int numEmptyNode = 0;
+ if (mApps.hasFilter()) {
+ // search result screen has only one SECTION_BREAK_VIEW
+ numEmptyNode = 1;
} else {
- return super.getRowCountForAccessibility(recycler, state);
+ // default all apps screen may have one or two SECTION_BREAK_VIEW
+ numEmptyNode = 1;
+ if (mApps.hasPredictedComponents()) {
+ if (viewType == PREDICTION_ICON_VIEW_TYPE) {
+ numEmptyNode = 1;
+ } else if (viewType == ICON_VIEW_TYPE) {
+ numEmptyNode = 2;
+ }
+ } else {
+ if (viewType == ICON_VIEW_TYPE) {
+ numEmptyNode = 1;
+ }
+ }
}
+ return numEmptyNode;
}
}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 26e9231..b533ce9 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -275,6 +275,10 @@
return (mSearchResults != null) && mFilteredApps.isEmpty();
}
+ public boolean hasPredictedComponents() {
+ return (mPredictedAppComponents != null && mPredictedAppComponents.size() > 0);
+ }
+
/**
* Sets the sorted list of filtered components.
*/
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
index f0221bc..811cacf 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
@@ -20,6 +20,7 @@
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
@@ -28,7 +29,9 @@
import com.android.launcher3.IconCache;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ComponentKey;
+import java.util.HashMap;
import java.util.List;
public abstract class AppWidgetManagerCompat {
@@ -62,6 +65,11 @@
return mAppWidgetManager.getAppWidgetInfo(appWidgetId);
}
+ public LauncherAppWidgetProviderInfo getLauncherAppWidgetInfo(int appWidgetId) {
+ AppWidgetProviderInfo info = getAppWidgetInfo(appWidgetId);
+ return info == null ? null : LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+ }
+
public abstract List<AppWidgetProviderInfo> getAllProviders();
public abstract String loadLabel(LauncherAppWidgetProviderInfo info);
@@ -81,4 +89,8 @@
public abstract Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap,
int imageWidth, int imageHeight);
+ public abstract LauncherAppWidgetProviderInfo findProvider(
+ ComponentName provider, UserHandleCompat user);
+
+ public abstract HashMap<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap();
}
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java
index e9d2510..de9414e 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java
@@ -21,6 +21,7 @@
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -31,7 +32,9 @@
import com.android.launcher3.IconCache;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ComponentKey;
+import java.util.HashMap;
import java.util.List;
class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat {
@@ -91,4 +94,25 @@
int imageWidth, int imageHeight) {
return bitmap;
}
+
+ @Override
+ public LauncherAppWidgetProviderInfo findProvider(
+ ComponentName provider, UserHandleCompat user) {
+ for (AppWidgetProviderInfo info : mAppWidgetManager.getInstalledProviders()) {
+ if (info.provider.equals(provider)) {
+ return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public HashMap<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap() {
+ HashMap<ComponentKey, AppWidgetProviderInfo> result = new HashMap<>();
+ UserHandleCompat user = UserHandleCompat.myUserHandle();
+ for (AppWidgetProviderInfo info : mAppWidgetManager.getInstalledProviders()) {
+ result.put(new ComponentKey(info.provider, user), info);
+ }
+ return result;
+ }
}
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
index 3bc3d0d..a1570e6 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
@@ -21,6 +21,7 @@
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -40,8 +41,10 @@
import com.android.launcher3.IconCache;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.R;
+import com.android.launcher3.util.ComponentKey;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@@ -145,4 +148,28 @@
c.setBitmap(null);
return bitmap;
}
+
+ @Override
+ public LauncherAppWidgetProviderInfo findProvider(ComponentName provider, UserHandleCompat user) {
+ for (AppWidgetProviderInfo info : mAppWidgetManager
+ .getInstalledProvidersForProfile(user.getUser())) {
+ if (info.provider.equals(provider)) {
+ return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public HashMap<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap() {
+ HashMap<ComponentKey, AppWidgetProviderInfo> result = new HashMap<>();
+ for (UserHandle user : mUserManager.getUserProfiles()) {
+ UserHandleCompat userHandle = UserHandleCompat.fromUser(user);
+ for (AppWidgetProviderInfo info :
+ mAppWidgetManager.getInstalledProvidersForProfile(user)) {
+ result.put(new ComponentKey(info.provider, userHandle), info);
+ }
+ }
+ return result;
+ }
}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
index 95e3ba9..237a9e9 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java
@@ -19,9 +19,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Rect;
import android.os.Bundle;
@@ -35,6 +32,10 @@
"android.intent.action.MANAGED_PROFILE_ADDED";
public static final String ACTION_MANAGED_PROFILE_REMOVED =
"android.intent.action.MANAGED_PROFILE_REMOVED";
+ public static final String ACTION_MANAGED_PROFILE_AVAILABLE =
+ "android.intent.action.MANAGED_PROFILE_AVAILABLE";
+ public static final String ACTION_MANAGED_PROFILE_UNAVAILABLE =
+ "android.intent.action.MANAGED_PROFILE_UNAVAILABLE";
public interface OnAppsChangedCallbackCompat {
void onPackageRemoved(String packageName, UserHandleCompat user);
@@ -42,6 +43,8 @@
void onPackageChanged(String packageName, UserHandleCompat user);
void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing);
void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing);
+ void onPackagesSuspended(String[] packageNames, UserHandleCompat user);
+ void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user);
}
protected LauncherAppsCompat() {
@@ -75,13 +78,5 @@
public abstract boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user);
public abstract boolean isActivityEnabledForProfile(ComponentName component,
UserHandleCompat user);
-
- public boolean isAppEnabled(PackageManager pm, String packageName, int flags) {
- try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, flags);
- return info != null && info.enabled;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-}
\ No newline at end of file
+ public abstract boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user);
+}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
index 339c457..4e2fc05 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
@@ -27,11 +27,11 @@
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Thunk;
import java.util.ArrayList;
@@ -114,7 +114,7 @@
}
public boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user) {
- return isAppEnabled(mPm, packageName, 0);
+ return PackageManagerHelper.isAppEnabled(mPm, packageName);
}
public boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user) {
@@ -126,6 +126,10 @@
}
}
+ public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) {
+ return false;
+ }
+
private void unregisterForPackageIntents() {
mContext.unregisterReceiver(mPackageMonitor);
}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
index fbf91b5..7270d02 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
@@ -36,7 +36,7 @@
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class LauncherAppsCompatVL extends LauncherAppsCompat {
- private LauncherApps mLauncherApps;
+ protected LauncherApps mLauncherApps;
private Map<OnAppsChangedCallbackCompat, WrappedCallback> mCallbacks
= new HashMap<OnAppsChangedCallbackCompat, WrappedCallback>();
@@ -106,6 +106,10 @@
return mLauncherApps.isActivityEnabled(component, user.getUser());
}
+ public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) {
+ return false;
+ }
+
private static class WrappedCallback extends LauncherApps.Callback {
private LauncherAppsCompat.OnAppsChangedCallbackCompat mCallback;
@@ -134,6 +138,14 @@
mCallback.onPackagesUnavailable(packageNames, UserHandleCompat.fromUser(user),
replacing);
}
+
+ public void onPackagesSuspended(String[] packageNames, UserHandle user) {
+ mCallback.onPackagesSuspended(packageNames, UserHandleCompat.fromUser(user));
+ }
+
+ public void onPackagesUnsuspended(String[] packageNames, UserHandle user) {
+ mCallback.onPackagesUnsuspended(packageNames, UserHandleCompat.fromUser(user));
+ }
}
}
diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java
index 9479908..50af21b 100644
--- a/src/com/android/launcher3/compat/UserHandleCompat.java
+++ b/src/com/android/launcher3/compat/UserHandleCompat.java
@@ -93,4 +93,14 @@
intent.putExtra(name, mUser);
}
}
+
+ public static UserHandleCompat fromIntent(Intent intent) {
+ if (Utilities.ATLEAST_LOLLIPOP) {
+ UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+ if (user != null) {
+ return UserHandleCompat.fromUser(user);
+ }
+ }
+ return null;
+ }
}
diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java
index 6b7cba8..978f922 100644
--- a/src/com/android/launcher3/compat/UserManagerCompat.java
+++ b/src/com/android/launcher3/compat/UserManagerCompat.java
@@ -32,7 +32,9 @@
public static UserManagerCompat getInstance(Context context) {
synchronized (sInstanceLock) {
if (sInstance == null) {
- if (Utilities.ATLEAST_LOLLIPOP) {
+ if (Utilities.isNycOrAbove()) {
+ sInstance = new UserManagerCompatVN(context.getApplicationContext());
+ } else if (Utilities.ATLEAST_LOLLIPOP) {
sInstance = new UserManagerCompatVL(context.getApplicationContext());
} else if (Utilities.ATLEAST_JB_MR1) {
sInstance = new UserManagerCompatV17(context.getApplicationContext());
@@ -54,4 +56,5 @@
public abstract UserHandleCompat getUserForSerialNumber(long serialNumber);
public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user);
public abstract long getUserCreationTime(UserHandleCompat user);
+ public abstract boolean isQuietModeEnabled(UserHandleCompat user);
}
diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java
index fcd7555..a006efd 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatV16.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java
@@ -50,4 +50,9 @@
@Override
public void enableAndResetCache() {
}
+
+ @Override
+ public boolean isQuietModeEnabled(UserHandleCompat user) {
+ return false;
+ }
}
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVN.java b/src/com/android/launcher3/compat/UserManagerCompatVN.java
new file mode 100644
index 0000000..ae41e68
--- /dev/null
+++ b/src/com/android/launcher3/compat/UserManagerCompatVN.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+//TODO: Once gogole3 SDK is updated to N, add @TargetApi(Build.VERSION_CODES.N)
+public class UserManagerCompatVN extends UserManagerCompatVL {
+
+ private static final String TAG = "UserManagerCompatVN";
+
+ UserManagerCompatVN(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isQuietModeEnabled(UserHandleCompat user) {
+ if (user != null) {
+ try {
+ //TODO: Replace with proper API call once google3 SDK is updated.
+ Method isQuietModeEnabledMethod = UserManager.class.getMethod("isQuietModeEnabled",
+ UserHandle.class);
+ return (boolean) isQuietModeEnabledMethod.invoke(mUserManager, user.getUser());
+ } catch (NoSuchMethodError | NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ Log.e(TAG, "Running on N without isQuietModeEnabled", e);
+ } catch (IllegalArgumentException e) {
+ // TODO remove this when API is fixed to not throw this
+ // when called on user that isn't a managed profile.
+ }
+ }
+ return false;
+ }
+}
+
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
new file mode 100644
index 0000000..9cab25a
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -0,0 +1,1028 @@
+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.net.Uri;
+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.backup.nano.BackupProtos;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.util.LongArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+/**
+ * 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 or device density change.
+ */
+public class GridSizeMigrationTask {
+
+ public static boolean ENABLED = Utilities.isNycOrAbove();
+
+ private static final String TAG = "GridSizeMigrationTask";
+ private static final boolean DEBUG = true;
+
+ private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
+ private static final String KEY_MIGRATION_SRC_HOTSEAT_SIZE = "migration_src_hotseat_size";
+
+ // Set of entries indicating minimum size a widget can be resized to. This is used during
+ // restore in case the widget has not been installed yet.
+ 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 least 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 InvariantDeviceProfile mIdp;
+
+ private final HashMap<String, Point> mWidgetMinSize = new HashMap<>();
+ private final ContentValues mTempValues = new ContentValues();
+ private final ArrayList<Long> mEntryToRemove = new ArrayList<>();
+ private final ArrayList<ContentProviderOperation> mUpdateOperations = new ArrayList<>();
+ private final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
+ private final HashSet<String> mValidPackages;
+
+ private final int mSrcX, mSrcY;
+ private final int mTrgX, mTrgY;
+ private final boolean mShouldRemoveX, mShouldRemoveY;
+
+ private final int mSrcHotseatSize;
+ private final int mSrcAllAppsRank;
+ private final int mDestHotseatSize;
+ private final int mDestAllAppsRank;
+
+ protected GridSizeMigrationTask(Context context, InvariantDeviceProfile idp,
+ HashSet<String> validPackages, HashMap<String, Point> widgetMinSize,
+ Point sourceSize, Point targetSize) {
+ mContext = context;
+ mValidPackages = validPackages;
+ mWidgetMinSize.putAll(widgetMinSize);
+ mIdp = idp;
+
+ mSrcX = sourceSize.x;
+ mSrcY = sourceSize.y;
+
+ mTrgX = targetSize.x;
+ mTrgY = targetSize.y;
+
+ mShouldRemoveX = mTrgX < mSrcX;
+ mShouldRemoveY = mTrgY < mSrcY;
+
+ // Non-used variables
+ mSrcHotseatSize = mSrcAllAppsRank = mDestHotseatSize = mDestAllAppsRank = -1;
+ }
+
+ protected GridSizeMigrationTask(Context context,
+ InvariantDeviceProfile idp, HashSet<String> validPackages,
+ int srcHotseatSize, int srcAllAppsRank,
+ int destHotseatSize, int destAllAppsRank) {
+ mContext = context;
+ mIdp = idp;
+ mValidPackages = validPackages;
+
+ mSrcHotseatSize = srcHotseatSize;
+ mSrcAllAppsRank = srcAllAppsRank;
+
+ mDestHotseatSize = destHotseatSize;
+ mDestAllAppsRank = destAllAppsRank;
+
+ // Non-used variables
+ mSrcX = mSrcY = mTrgX = mTrgY = -1;
+ mShouldRemoveX = mShouldRemoveY = false;
+ }
+
+ /**
+ * Applied all the pending DB operations
+ * @return true if any DB operation was commited.
+ */
+ private boolean applyOperations() throws Exception {
+ // Update items
+ if (!mUpdateOperations.isEmpty()) {
+ 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);
+ }
+
+ return !mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty();
+ }
+
+ /**
+ * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them
+ * in the order in the new hotseat while keeping an empty space for all-apps. If the number of
+ * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
+ * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
+ * & {@see #WT_FOLDER_FACTOR}.
+ * @return true if any DB change was made
+ */
+ protected boolean migrateHotseat() throws Exception {
+ ArrayList<DbEntry> items = loadHotseatEntries();
+
+ int requiredCount = mDestHotseatSize - 1;
+
+ while (items.size() > requiredCount) {
+ // Pick the center item by default.
+ DbEntry toRemove = items.get(items.size() / 2);
+
+ // Find the item with least weight.
+ for (DbEntry entry : items) {
+ if (entry.weight < toRemove.weight) {
+ toRemove = entry;
+ }
+ }
+
+ mEntryToRemove.add(toRemove.id);
+ items.remove(toRemove);
+ }
+
+ // Update screen IDS
+ int newScreenId = 0;
+ for (DbEntry entry : items) {
+ if (entry.screenId != newScreenId) {
+ entry.screenId = newScreenId;
+
+ // These values does not affect the item position, but we should set them
+ // to something other than -1.
+ entry.cellX = newScreenId;
+ entry.cellY = 0;
+
+ update(entry);
+ }
+
+ newScreenId++;
+ if (newScreenId == mDestAllAppsRank) {
+ newScreenId++;
+ }
+ }
+
+ return applyOperations();
+ }
+
+ /**
+ * @return true if any DB change was made
+ */
+ protected boolean migrateWorkspace() throws Exception {
+ 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());
+
+ // Update screens
+ final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
+ mUpdateOperations.add(ContentProviderOperation.newDelete(uri).build());
+ int count = allScreens.size();
+ for (int i = 0; i < count; i++) {
+ ContentValues v = new ContentValues();
+ long screenId = allScreens.get(i);
+ v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
+ v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+ mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
+ }
+ }
+ return applyOperations();
+ }
+
+ /**
+ * 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 = loadWorkspaceEntries(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;
+ }
+
+ private 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;
+ }
+ }
+ }
+
+ private 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);
+ }
+ }
+ }
+ }
+
+ private ArrayList<DbEntry> loadHotseatEntries() {
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{
+ Favorites._ID, // 0
+ Favorites.ITEM_TYPE, // 1
+ Favorites.INTENT, // 2
+ Favorites.SCREEN}, // 3
+ Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null);
+
+ final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
+ final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
+ final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
+ final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN);
+
+ ArrayList<DbEntry> entries = new ArrayList<>();
+ while (c.moveToNext()) {
+ DbEntry entry = new DbEntry();
+ entry.id = c.getLong(indexId);
+ entry.itemType = c.getInt(indexItemType);
+ entry.screenId = c.getLong(indexScreen);
+
+ if (entry.screenId >= mSrcHotseatSize) {
+ mEntryToRemove.add(entry.id);
+ continue;
+ }
+
+ 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_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);
+ }
+ c.close();
+ return entries;
+ }
+
+
+ /**
+ * Loads entries for a particular screen id.
+ */
+ private ArrayList<DbEntry> loadWorkspaceEntries(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.APPWIDGET_ID}, // 8
+ 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);
+ final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
+
+ 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);
+
+ int widgetId = c.getInt(indexAppWidgetId);
+ LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance(
+ mContext).getLauncherAppWidgetInfo(widgetId);
+ 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);
+ }
+ c.close();
+ 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));
+ }
+ }
+ c.close();
+ 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);
+ }
+ }
+
+ private 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]));
+ }
+
+ private static String getPointString(int x, int y) {
+ return String.format(Locale.ENGLISH, "%d,%d", x, y);
+ }
+
+ public static void markForMigration(
+ Context context, HashSet<String> widgets, BackupProtos.DeviceProfieData srcProfile) {
+ Utilities.getPrefs(context).edit()
+ .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE,
+ getPointString((int) srcProfile.desktopCols, (int) srcProfile.desktopRows))
+ .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE,
+ getPointString((int) srcProfile.hotseatCount, srcProfile.allappsRank))
+ .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets)
+ .apply();
+ }
+
+ /**
+ * Migrates the workspace and hotseat in case their sizes changed.
+ * @return false if the migration failed.
+ */
+ public static boolean migrateGridIfNeeded(Context context) {
+ SharedPreferences prefs = Utilities.getPrefs(context);
+ InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile();
+
+ String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+ String hotseatSizeString = getPointString(idp.numHotseatIcons, idp.hotseatAllAppsRank);
+
+ if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
+ hotseatSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, ""))) {
+ // Skip if workspace and hotseat sizes have not changed.
+ return true;
+ }
+
+ long migrationStartTime = System.currentTimeMillis();
+ try {
+ boolean dbChanged = false;
+
+ // 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.
+ HashSet validPackages = new HashSet<>();
+ for (PackageInfo info : context.getPackageManager().getInstalledPackages(0)) {
+ validPackages.add(info.packageName);
+ }
+ validPackages.addAll(PackageInstallerCompat.getInstance(context)
+ .updateAndGetActiveSessionCache().keySet());
+
+ // Hotseat
+ Point srcHotseatSize = parsePoint(prefs.getString(
+ KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString));
+ if (srcHotseatSize.x != idp.numHotseatIcons ||
+ srcHotseatSize.y != idp.hotseatAllAppsRank) {
+ // Migrate hotseat.
+
+ dbChanged = new GridSizeMigrationTask(context,
+ LauncherAppState.getInstance().getInvariantDeviceProfile(),
+ validPackages,
+ srcHotseatSize.x, srcHotseatSize.y,
+ idp.numHotseatIcons, idp.hotseatAllAppsRank).migrateHotseat();
+ }
+
+ // Grid size
+ Point targetSize = new Point(idp.numColumns, idp.numRows);
+ Point sourceSize = parsePoint(prefs.getString(
+ KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
+
+ if (!targetSize.equals(sourceSize)) {
+
+ // The following list defines all possible grid sizes (and intermediate steps
+ // during migration). Note that at each step, dx <= 1 && dy <= 1. Any grid size
+ // which is not in this list is not migrated.
+ ArrayList<Point> gridSizeSteps = new ArrayList<>();
+ gridSizeSteps.add(new Point(2, 3));
+ gridSizeSteps.add(new Point(3, 3));
+ gridSizeSteps.add(new Point(3, 4));
+ gridSizeSteps.add(new Point(4, 4));
+ gridSizeSteps.add(new Point(5, 5));
+ gridSizeSteps.add(new Point(5, 6));
+ gridSizeSteps.add(new Point(6, 6));
+ gridSizeSteps.add(new Point(7, 7));
+
+ int sourceSizeIndex = gridSizeSteps.indexOf(sourceSize);
+ int targetSizeIndex = gridSizeSteps.indexOf(targetSize);
+
+ if (sourceSizeIndex <= -1 || targetSizeIndex <= -1) {
+ throw new Exception("Unable to migrate grid size from " + sourceSize
+ + " to " + targetSize);
+ }
+
+ // Min widget sizes
+ HashMap<String, Point> widgetMinSize = new HashMap<>();
+ for (String s : Utilities.getPrefs(context).getStringSet(KEY_MIGRATION_WIDGET_MINSIZE,
+ Collections.<String>emptySet())) {
+ String[] parts = s.split("#");
+ widgetMinSize.put(parts[0], parsePoint(parts[1]));
+ }
+
+ // Migrate the workspace grid, step by step.
+ while (targetSizeIndex < sourceSizeIndex ) {
+ // We only need to migrate the grid if source size is greater
+ // than the target size.
+ Point stepTargetSize = gridSizeSteps.get(sourceSizeIndex - 1);
+ Point stepSourceSize = gridSizeSteps.get(sourceSizeIndex);
+
+ if (new GridSizeMigrationTask(context,
+ LauncherAppState.getInstance().getInvariantDeviceProfile(),
+ validPackages, widgetMinSize,
+ stepSourceSize, stepTargetSize).migrateWorkspace()) {
+ dbChanged = true;
+ }
+ sourceSizeIndex--;
+ }
+ }
+
+ if (dbChanged) {
+ // Make sure we haven't removed everything.
+ final Cursor c = context.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");
+ }
+ }
+
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Error during grid migration", e);
+
+ return false;
+ } finally {
+ Log.v(TAG, "Workspace migration completed in "
+ + (System.currentTimeMillis() - migrationStartTime));
+
+ // Save current configuration, so that the migration does not run again.
+ prefs.edit()
+ .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+ .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString)
+ .remove(KEY_MIGRATION_WIDGET_MINSIZE)
+ .apply();
+ }
+ }
+}
diff --git a/src/com/android/launcher3/model/MigrateFromRestoreTask.java b/src/com/android/launcher3/model/MigrateFromRestoreTask.java
deleted file mode 100644
index 786ab60..0000000
--- a/src/com/android/launcher3/model/MigrateFromRestoreTask.java
+++ /dev/null
@@ -1,763 +0,0 @@
-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 = Utilities.getPrefs(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);
- }
- c.close();
- 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));
- }
- }
- c.close();
- 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) {
- Utilities.getPrefs(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(Utilities.getPrefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, ""));
- }
-
- public static void clearFlags(Context context) {
- Utilities.getPrefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE)
- .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit();
- }
-
-}
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 99a53ff..53e5213 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -1,9 +1,13 @@
package com.android.launcher3.model;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ResolveInfo;
+import android.os.DeadObjectException;
+import android.os.TransactionTooLargeException;
import android.util.Log;
import com.android.launcher3.AppFilter;
@@ -16,6 +20,7 @@
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.config.ProviderConfig;
import java.util.ArrayList;
import java.util.Collections;
@@ -95,8 +100,41 @@
return mRawList;
}
- public void setWidgetsAndShortcuts(ArrayList<Object> rawWidgetsShortcuts) {
+ public boolean isEmpty() {
+ return mRawList.isEmpty();
+ }
+
+ public WidgetsModel updateAndClone(Context context) {
Utilities.assertWorkerThread();
+
+ try {
+ final ArrayList<Object> widgetsAndShortcuts = new ArrayList<>();
+ // Widgets
+ for (AppWidgetProviderInfo widgetInfo :
+ AppWidgetManagerCompat.getInstance(context).getAllProviders()) {
+ widgetsAndShortcuts.add(LauncherAppWidgetProviderInfo
+ .fromProviderInfo(context, widgetInfo));
+ }
+ // Shortcuts
+ widgetsAndShortcuts.addAll(context.getPackageManager().queryIntentActivities(
+ new Intent(Intent.ACTION_CREATE_SHORTCUT), 0));
+ setWidgetsAndShortcuts(widgetsAndShortcuts);
+ } catch (Exception e) {
+ if (!LauncherAppState.isDogfoodBuild() &&
+ (e.getCause() instanceof TransactionTooLargeException ||
+ e.getCause() instanceof DeadObjectException)) {
+ // the returned value may be incomplete and will not be refreshed until the next
+ // time Launcher starts.
+ // TODO: after figuring out a repro step, introduce a dirty bit to check when
+ // onResume is called to refresh the widget provider list.
+ } else {
+ throw e;
+ }
+ }
+ return clone();
+ }
+
+ private void setWidgetsAndShortcuts(ArrayList<Object> rawWidgetsShortcuts) {
mRawList = rawWidgetsShortcuts;
if (DEBUG) {
Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
index c61fa88..bdb1639 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -23,26 +23,30 @@
import android.content.res.Configuration;
import android.util.Log;
+import com.android.launcher3.Utilities;
+
/**
* {@link BroadcastReceiver} which watches configuration changes and
- * restarts the process in case changes which affect the device profile.
+ * restarts the process in case changes which affect the device profile occur.
*/
public class ConfigMonitor extends BroadcastReceiver {
private final Context mContext;
private final float mFontScale;
+ private final int mDensity;
public ConfigMonitor(Context context) {
mContext = context;
Configuration config = context.getResources().getConfiguration();
mFontScale = config.fontScale;
+ mDensity = getDensity(config);
}
@Override
public void onReceive(Context context, Intent intent) {
Configuration config = context.getResources().getConfiguration();
- if (mFontScale != config.fontScale) {
+ if (mFontScale != config.fontScale || mDensity != getDensity(config)) {
Log.d("ConfigMonitor", "Configuration changed, restarting launcher");
mContext.unregisterReceiver(this);
android.os.Process.killProcess(android.os.Process.myPid());
@@ -52,4 +56,8 @@
public void register() {
mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
}
+
+ private static int getDensity(Configuration config) {
+ return Utilities.ATLEAST_JB_MR1 ? config.densityDpi : 0;
+ }
}
diff --git a/src/com/android/launcher3/util/FlagOp.java b/src/com/android/launcher3/util/FlagOp.java
new file mode 100644
index 0000000..5e26ed1
--- /dev/null
+++ b/src/com/android/launcher3/util/FlagOp.java
@@ -0,0 +1,30 @@
+package com.android.launcher3.util;
+
+public abstract class FlagOp {
+
+ public static FlagOp NO_OP = new FlagOp() {};
+
+ private FlagOp() {}
+
+ public int apply(int flags) {
+ return flags;
+ }
+
+ public static FlagOp addFlag(final int flag) {
+ return new FlagOp() {
+ @Override
+ public int apply(int flags) {
+ return flags | flag;
+ }
+ };
+ }
+
+ public static FlagOp removeFlag(final int flag) {
+ return new FlagOp() {
+ @Override
+ public int apply(int flags) {
+ return flags & ~flag;
+ }
+ };
+ }
+}
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
new file mode 100644
index 0000000..d034572
--- /dev/null
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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.util;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+/**
+ * Utility methods using package manager
+ */
+public class PackageManagerHelper {
+
+ private static final int FLAG_SUSPENDED = 1<<30;
+
+ /**
+ * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't
+ * guarantee that the app is on SD card.
+ */
+ public static boolean isAppOnSdcard(PackageManager pm, String packageName) {
+ return isAppEnabled(pm, packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
+ }
+
+ public static boolean isAppEnabled(PackageManager pm, String packageName) {
+ return isAppEnabled(pm, packageName, 0);
+ }
+
+ public static boolean isAppEnabled(PackageManager pm, String packageName, int flags) {
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, flags);
+ return info != null && info.enabled;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static boolean isAppSuspended(PackageManager pm, String packageName) {
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ return info != null && isAppSuspended(info);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static boolean isAppSuspended(ApplicationInfo info) {
+ return (info.flags & FLAG_SUSPENDED) != 0;
+ }
+}
diff --git a/src/com/android/launcher3/util/StringFilter.java b/src/com/android/launcher3/util/StringFilter.java
new file mode 100644
index 0000000..f539ad1
--- /dev/null
+++ b/src/com/android/launcher3/util/StringFilter.java
@@ -0,0 +1,31 @@
+package com.android.launcher3.util;
+
+import java.util.Set;
+
+/**
+ * Abstract class to filter a set of strings.
+ */
+public abstract class StringFilter {
+
+ private StringFilter() { }
+
+ public abstract boolean matches(String str);
+
+ public static StringFilter matchesAll() {
+ return new StringFilter() {
+ @Override
+ public boolean matches(String str) {
+ return true;
+ }
+ };
+ }
+
+ public static StringFilter of(final Set<String> validEntries) {
+ return new StringFilter() {
+ @Override
+ public boolean matches(String str) {
+ return validEntries.contains(str);
+ }
+ };
+ }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 10a00c6..c8e8cf8 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -332,6 +332,10 @@
mAdapter.notifyDataSetChanged();
}
+ public boolean isEmpty() {
+ return mAdapter.getItemCount() == 0;
+ }
+
private WidgetPreviewLoader getWidgetPreviewLoader() {
if (mWidgetPreviewLoader == null) {
mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache();
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
new file mode 100644
index 0000000..ec157bc
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -0,0 +1,317 @@
+package com.android.launcher3.model;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.test.ProviderTestCase2;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.TestLauncherProvider;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Unit tests for {@link GridSizeMigrationTask}
+ */
+public class GridSizeMigrationTaskTest extends ProviderTestCase2<TestLauncherProvider> {
+
+ private static final long DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ private static final long HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
+ private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+
+ private static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
+ private static final String VALID_INTENT =
+ new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0);
+
+ private HashSet<String> mValidPackages;
+ private InvariantDeviceProfile mIdp;
+
+ public GridSizeMigrationTaskTest() {
+ super(TestLauncherProvider.class, ProviderConfig.AUTHORITY);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mValidPackages = new HashSet<>();
+ mValidPackages.add(TEST_PACKAGE);
+
+ mIdp = new InvariantDeviceProfile();
+ }
+
+ public void testHotseatMigration_apps_dropped() throws Exception {
+ long[] hotseatItems = {
+ addItem(APPLICATION, 0, HOTSEAT, 0, 0),
+ addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
+ -1,
+ addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+ addItem(APPLICATION, 4, HOTSEAT, 0, 0),
+ };
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 2, 3, 1)
+ .migrateHotseat();
+ // First & last items are dropped as they have the least weight.
+ verifyHotseat(hotseatItems[1], -1, hotseatItems[3]);
+ }
+
+ public void testHotseatMigration_shortcuts_dropped() throws Exception {
+ long[] hotseatItems = {
+ addItem(APPLICATION, 0, HOTSEAT, 0, 0),
+ addItem(30, 1, HOTSEAT, 0, 0),
+ -1,
+ addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+ addItem(10, 4, HOTSEAT, 0, 0),
+ };
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 2, 3, 1)
+ .migrateHotseat();
+ // First & third items are dropped as they have the least weight.
+ verifyHotseat(hotseatItems[1], -1, hotseatItems[4]);
+ }
+
+ private void verifyHotseat(long... sortedIds) {
+ int screenId = 0;
+ int total = 0;
+
+ for (long id : sortedIds) {
+ Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites._ID},
+ "container=-101 and screen=" + screenId, null, null, null);
+
+ if (id == -1) {
+ assertEquals(0, c.getCount());
+ } else {
+ assertEquals(1, c.getCount());
+ c.moveToNext();
+ assertEquals(id, c.getLong(0));
+ total ++;
+ }
+ c.close();
+
+ screenId++;
+ }
+
+ // Verify that not other entry exist in the DB.
+ Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites._ID},
+ "container=-101", null, null, null);
+ assertEquals(total, c.getCount());
+ c.close();
+ }
+
+ public void testWorkspace_empty_row_column_removed() throws Exception {
+ long[][][] ids = createGrid(new int[][][]{{
+ { 0, 0, -1, 1},
+ { 3, 1, -1, 4},
+ { -1, -1, -1, -1},
+ { 5, 2, -1, 6},
+ }});
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 3)).migrateWorkspace();
+
+ // Column 2 and row 2 got removed.
+ verifyWorkspace(new long[][][] {{
+ {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
+ {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
+ }});
+ }
+
+ public void testWorkspace_new_screen_created() throws Exception {
+ long[][][] ids = createGrid(new int[][][]{{
+ { 0, 0, 0, 1},
+ { 3, 1, 0, 4},
+ { -1, -1, -1, -1},
+ { 5, 2, -1, 6},
+ }});
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 3)).migrateWorkspace();
+
+ // Items in the second column get moved to new screen
+ verifyWorkspace(new long[][][] {{
+ {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
+ {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
+ }, {
+ {ids[0][0][2], ids[0][1][2], -1},
+ }});
+ }
+
+ public void testWorkspace_items_merged_in_next_screen() throws Exception {
+ long[][][] ids = createGrid(new int[][][]{{
+ { 0, 0, 0, 1},
+ { 3, 1, 0, 4},
+ { -1, -1, -1, -1},
+ { 5, 2, -1, 6},
+ },{
+ { 0, 0, -1, 1},
+ { 3, 1, -1, 4},
+ }});
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 3)).migrateWorkspace();
+
+ // Items in the second column of the first screen should get placed on the 3rd
+ // row of the second screen
+ verifyWorkspace(new long[][][] {{
+ {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
+ {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
+ }, {
+ {ids[1][0][0], ids[1][0][1], ids[1][0][3]},
+ {ids[1][1][0], ids[1][1][1], ids[1][1][3]},
+ {ids[0][0][2], ids[0][1][2], -1},
+ }});
+ }
+
+ public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
+ // First screen has 2 items that need to be moved, but second screen has only one
+ // empty space after migration (top-left corner)
+ long[][][] ids = createGrid(new int[][][]{{
+ { 0, 0, 0, 1},
+ { 3, 1, 0, 4},
+ { -1, -1, -1, -1},
+ { 5, 2, -1, 6},
+ },{
+ { -1, 0, -1, 1},
+ { 3, 1, -1, 4},
+ { -1, -1, -1, -1},
+ { 5, 2, -1, 6},
+ }});
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 3)).migrateWorkspace();
+
+ // Items in the second column of the first screen should get placed on a new screen.
+ verifyWorkspace(new long[][][] {{
+ {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
+ {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
+ }, {
+ { -1, ids[1][0][1], ids[1][0][3]},
+ {ids[1][1][0], ids[1][1][1], ids[1][1][3]},
+ {ids[1][3][0], ids[1][3][1], ids[1][3][3]},
+ }, {
+ {ids[0][0][2], ids[0][1][2], -1},
+ }});
+ }
+
+ /**
+ * Initializes the DB with dummy elements to represent the provided grid structure.
+ * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
+ * type definitions. The first dimension represents the screens and the next
+ * two represent the workspace grid.
+ * @return the same grid representation where each entry is the corresponding item id.
+ */
+ private long[][][] createGrid(int[][][] typeArray) throws Exception {
+ long[][][] ids = new long[typeArray.length][][];
+
+ for (int i = 0; i < typeArray.length; i++) {
+ // Add screen to DB
+ long screenId = LauncherAppState.getLauncherProvider().generateNewScreenId();
+ ContentValues v = new ContentValues();
+ v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
+ v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+ getMockContentResolver().insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v);
+
+ ids[i] = new long[typeArray[i].length][];
+ for (int y = 0; y < typeArray[i].length; y++) {
+ ids[i][y] = new long[typeArray[i][y].length];
+ for (int x = 0; x < typeArray[i][y].length; x++) {
+ if (typeArray[i][y][x] < 0) {
+ // Empty cell
+ ids[i][y][x] = -1;
+ } else {
+ ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
+ }
+ }
+ }
+ }
+ return ids;
+ }
+
+ /**
+ * Verifies that the workspace items are arranged in the provided order.
+ * @param ids A 3d array where the first dimension represents the screen, and the rest two
+ * represent the workspace grid.
+ */
+ private void verifyWorkspace(long[][][] ids) {
+ ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(getMockContext());
+ assertEquals(ids.length, allScreens.size());
+ int total = 0;
+
+ for (int i = 0; i < ids.length; i++) {
+ long screenId = allScreens.get(i);
+ for (int y = 0; y < ids[i].length; y++) {
+ for (int x = 0; x < ids[i][y].length; x++) {
+ long id = ids[i][y][x];
+
+ Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites._ID},
+ "container=-100 and screen=" + screenId +
+ " and cellX=" + x + " and cellY=" + y, null, null, null);
+ if (id == -1) {
+ assertEquals(0, c.getCount());
+ } else {
+ assertEquals(1, c.getCount());
+ c.moveToNext();
+ assertEquals(id, c.getLong(0));
+ total++;
+ }
+ c.close();
+ }
+ }
+ }
+
+ // Verify that not other entry exist in the DB.
+ Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites._ID},
+ "container=-100", null, null, null);
+ assertEquals(total, c.getCount());
+ c.close();
+ }
+
+ /**
+ * Adds a dummy item in the DB.
+ * @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for
+ * folder (where the type represents the number of items in the folder).
+ */
+ private long addItem(int type, long screen, long container, int x, int y) throws Exception {
+ long id = LauncherAppState.getLauncherProvider().generateNewItemId();
+
+ ContentValues values = new ContentValues();
+ values.put(LauncherSettings.Favorites._ID, id);
+ values.put(LauncherSettings.Favorites.CONTAINER, container);
+ values.put(LauncherSettings.Favorites.SCREEN, screen);
+ values.put(LauncherSettings.Favorites.CELLX, x);
+ values.put(LauncherSettings.Favorites.CELLY, y);
+ values.put(LauncherSettings.Favorites.SPANX, 1);
+ values.put(LauncherSettings.Favorites.SPANY, 1);
+
+ if (type == APPLICATION || type == SHORTCUT) {
+ values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
+ values.put(LauncherSettings.Favorites.INTENT, VALID_INTENT);
+ } else {
+ values.put(LauncherSettings.Favorites.ITEM_TYPE,
+ LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
+ // Add folder items.
+ for (int i = 0; i < type; i++) {
+ addItem(APPLICATION, 0, id, 0, 0);
+ }
+ }
+
+ getMockContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
+ return id;
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/TestLauncherProvider.java b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
new file mode 100644
index 0000000..8758f55
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
@@ -0,0 +1,41 @@
+package com.android.launcher3.util;
+
+import android.content.Context;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherProvider;
+
+/**
+ * An extension of LauncherProvider backed up by in-memory database.
+ */
+public class TestLauncherProvider extends LauncherProvider {
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ protected synchronized void createDbIfNotExists() {
+ if (mOpenHelper == null) {
+ mOpenHelper = new MyDatabaseHelper(getContext());
+ }
+ }
+
+ @Override
+ protected void notifyListeners() { }
+
+ private static class MyDatabaseHelper extends DatabaseHelper {
+ public MyDatabaseHelper(Context context) {
+ super(context, null);
+ }
+
+ @Override
+ protected long getDefaultUserSerial() {
+ return 0;
+ }
+
+ @Override
+ protected void onEmptyDbCreated() { }
+ }
+}
\ No newline at end of file