From 651077bdd603bb182be039925fd17bdf0da15016 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 30 Jun 2014 14:15:31 -0700 Subject: Adding app widget restore support in Launcher3 for android L When the app is restored, it displays placeholders for all pending widgets. These placeholders can be moved and removed similar to a widget (size is fixed to what defined in backup). Once the system notifies the launcher of the new widget ids, the place holders are replaced with actual widgets. issue: 10779035 Change-Id: I68cbe4da01e9ca2978cb4471a7c645d2aa592055 --- .../launcher3/AppWidgetsRestoredReceiver.java | 90 ++++++++++++++++++++++ src/com/android/launcher3/DeleteDropTarget.java | 2 +- src/com/android/launcher3/Launcher.java | 24 ++++-- .../launcher3/LauncherAppWidgetHostView.java | 26 +++++++ .../android/launcher3/LauncherAppWidgetInfo.java | 22 ++++++ src/com/android/launcher3/LauncherModel.java | 76 +++++++++++++----- src/com/android/launcher3/Workspace.java | 64 ++++++++++++--- 7 files changed, 264 insertions(+), 40 deletions(-) create mode 100644 src/com/android/launcher3/AppWidgetsRestoredReceiver.java (limited to 'src/com/android') diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java new file mode 100644 index 000000000..9fef7f934 --- /dev/null +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -0,0 +1,90 @@ +package com.android.launcher3; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.AsyncTask; +import android.util.Log; + +import com.android.launcher3.LauncherSettings.Favorites; + +import java.util.ArrayList; +import java.util.List; + +public class AppWidgetsRestoredReceiver extends BroadcastReceiver { + + private static final String TAG = "AppWidgetsRestoredReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED.equals(intent.getAction())) { + int[] oldIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS); + int[] newIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (oldIds.length == newIds.length) { + restoreAppWidgetIds(context, oldIds, newIds); + } else { + Log.e(TAG, "Invalid host restored received"); + } + } + } + + /** + * Updates the app widgets whose id has changed during the restore process. + */ + static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + final ContentResolver cr = context.getContentResolver(); + final List idsToRemove = new ArrayList<>(); + final AppWidgetManager widgets = AppWidgetManager.getInstance(context); + + for (int i = 0; i < oldWidgetIds.length; i++) { + Log.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]); + + final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]); + + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i]); + values.put(LauncherSettings.Favorites.RESTORED, LauncherModel.isValidProvider(provider) + ? LauncherAppWidgetInfo.RESTORE_COMPLETED + : LauncherAppWidgetInfo.RESTORE_PROVIDER_PENDING); + + String[] widgetIdParams = new String[] { Integer.toString(oldWidgetIds[i]) }; + + int result = cr.update(Favorites.CONTENT_URI, values, + "appWidgetId=? and restored=1", widgetIdParams); + if (result == 0) { + Cursor cursor = cr.query(Favorites.CONTENT_URI, + new String[] {Favorites.APPWIDGET_ID}, + "appWidgetId=?", widgetIdParams, null); + try { + if (!cursor.moveToFirst()) { + // The widget no long exists. + idsToRemove.add(newWidgetIds[i]); + } + } finally { + cursor.close(); + } + } + } + // Unregister the widget IDs which are not present on the workspace. This could happen + // when a widget place holder is removed from workspace, before this method is called. + if (!idsToRemove.isEmpty()) { + final AppWidgetHost appWidgetHost = + new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); + new AsyncTask() { + public Void doInBackground(Void ... args) { + for (Integer id : idsToRemove) { + appWidgetHost.deleteAppWidgetId(id); + Log.e(TAG, "Widget no longer present, appWidgetId=" + id); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } + } +} diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 3d45432c2..4c3388e05 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -326,7 +326,7 @@ public class DeleteDropTarget extends ButtonDropTarget { final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); - if (appWidgetHost != null) { + if ((appWidgetHost != null) && launcherAppWidgetInfo.isWidgetIdValid()) { // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... new AsyncTask() { diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index d3a436237..5e05f927d 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -89,6 +89,7 @@ import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AccelerateDecelerateInterpolator; @@ -102,6 +103,8 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.PagedView.PageSwitchListener; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; @@ -4387,13 +4390,20 @@ public class Launcher extends Activity } final Workspace workspace = mWorkspace; - final int appWidgetId = item.appWidgetId; - final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); - if (DEBUG_WIDGETS) { - Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); - } + final AppWidgetProviderInfo appWidgetInfo; + if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { + final int appWidgetId = item.appWidgetId; + appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + if (DEBUG_WIDGETS) { + Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); + } - item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + } else { + appWidgetInfo = null; + item.hostView = new LauncherAppWidgetHostView(this, false); + item.hostView.updateAppWidget(null); + } item.hostView.setTag(item); item.onBindAppWidget(this); @@ -4553,7 +4563,7 @@ public class Launcher extends Activity } if (mWorkspace != null) { - mWorkspace.updateShortcuts(apps); + mWorkspace.updateShortcutsAndWidgets(apps); } if (!LauncherAppState.isDisableAllApps() && diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java index f47fd13ec..7eb005255 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -18,6 +18,7 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; import android.content.Context; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -39,12 +40,28 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc private float mSlop; + private boolean mWidgetReady; + public LauncherAppWidgetHostView(Context context) { + this(context, true); + } + + public LauncherAppWidgetHostView(Context context, boolean widgetReady) { super(context); mContext = context; mLongPressHelper = new CheckLongPressHelper(this); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDragLayer = ((Launcher) context).getDragLayer(); + mWidgetReady = widgetReady; + } + + @Override + public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, + int maxHeight) { + // If the widget is not yet ready, the app widget size cannot be updated. + if (mWidgetReady) { + super.updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight); + } } @Override @@ -52,6 +69,15 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc return mInflater.inflate(R.layout.appwidget_error, this, false); } + @Override + protected View getDefaultView() { + if (mWidgetReady) { + return super.getDefaultView(); + } else { + return mInflater.inflate(R.layout.appwidget_not_ready, this, false); + } + } + @Override public void updateAppWidget(RemoteViews remoteViews) { // Store the orientation in which the widget was inflated diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index bec1f9eb3..b3ac12b37 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -28,6 +28,18 @@ import com.android.launcher3.compat.UserHandleCompat; */ public class LauncherAppWidgetInfo extends ItemInfo { + public static final int RESTORE_COMPLETED = 0; + + /** + * This is set during the package backup creation. + */ + public static final int RESTORE_REMAP_PENDING = 1; + + /** + * Widget provider is not yet installed. + */ + public static final int RESTORE_PROVIDER_PENDING = 2; + /** * Indicates that the widget hasn't been instantiated yet. */ @@ -45,6 +57,11 @@ public class LauncherAppWidgetInfo extends ItemInfo { int minWidth = -1; int minHeight = -1; + /** + * Indicates the restore status of the widget. + */ + int restoreStatus; + private boolean mHasNotifiedInitialWidgetSizeChanged; /** @@ -64,6 +81,7 @@ public class LauncherAppWidgetInfo extends ItemInfo { spanY = -1; // We only support app widgets on current user. user = UserHandleCompat.myUserHandle(); + restoreStatus = RESTORE_COMPLETED; } @Override @@ -101,4 +119,8 @@ public class LauncherAppWidgetInfo extends ItemInfo { super.unbind(); hostView = null; } + + public final boolean isWidgetIdValid() { + return restoreStatus != RESTORE_REMAP_PENDING; + } } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index a8bace8e2..db5e7cf68 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -2125,31 +2125,54 @@ public class LauncherModel extends BroadcastReceiver // Read all Launcher-specific widget details int appWidgetId = c.getInt(appWidgetIdIndex); String savedProvider = c.getString(appWidgetProviderIndex); - id = c.getLong(idIndex); - final AppWidgetProviderInfo provider = - widgets.getAppWidgetInfo(appWidgetId); - - if (!isSafeMode && (provider == null || provider.provider == null || - provider.provider.getPackageName() == null)) { - String log = "Deleting widget that isn't installed anymore: id=" - + id + " appWidgetId=" + appWidgetId; + final int restoreStatus = c.getInt(restoredIndex); + final boolean restorePending = Utilities.isLmp() + && (restoreStatus == + LauncherAppWidgetInfo.RESTORE_REMAP_PENDING); + final boolean providerPending = Utilities.isLmp() + && (restoreStatus == + LauncherAppWidgetInfo.RESTORE_PROVIDER_PENDING); + + // Do not try to get the provider if restore is pending, as the + // widget id is invalid, and it might point to some other provider. + final AppWidgetProviderInfo provider = restorePending ? null + : widgets.getAppWidgetInfo(appWidgetId); + boolean providerValid = isValidProvider(provider); + + // Skip provider check, + // 1. when the widget id re-map is pending + // 2. provider is pending install for a restored widget + if (!isSafeMode && !providerPending && !restorePending + && !providerValid) { + String log = "Deleting widget that isn't installed anymore: " + + "id=" + id + " appWidgetId=" + appWidgetId; Log.e(TAG, log); Launcher.addDumpLog(TAG, log, false); itemsToRemove.add(id); } else { - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, - provider.provider); + if (providerValid) { + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + provider.provider); + int[] minSpan = + Launcher.getMinSpanForWidget(context, provider); + appWidgetInfo.minSpanX = minSpan[0]; + appWidgetInfo.minSpanY = minSpan[1]; + } else { + Log.v(TAG, "Widget restore pending id=" + id + + " appWidgetId=" + appWidgetId + + " status =" + restoreStatus); + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + ComponentName.unflattenFromString(savedProvider)); + appWidgetInfo.restoreStatus = restoreStatus; + } appWidgetInfo.id = id; appWidgetInfo.screenId = c.getInt(screenIndex); appWidgetInfo.cellX = c.getInt(cellXIndex); appWidgetInfo.cellY = c.getInt(cellYIndex); appWidgetInfo.spanX = c.getInt(spanXIndex); appWidgetInfo.spanY = c.getInt(spanYIndex); - int[] minSpan = Launcher.getMinSpanForWidget(context, provider); - appWidgetInfo.minSpanX = minSpan[0]; - appWidgetInfo.minSpanY = minSpan[1]; container = c.getInt(containerIndex); if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && @@ -2169,14 +2192,20 @@ public class LauncherModel extends BroadcastReceiver } break; } - String providerName = provider.provider.flattenToString(); - if (!providerName.equals(savedProvider)) { - ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, - providerName); - String where = BaseColumns._ID + "= ?"; - String[] args = {Integer.toString(c.getInt(idIndex))}; - contentResolver.update(contentUri, values, where, args); + + if (providerValid) { + String providerName = provider.provider.flattenToString(); + + if (!providerName.equals(savedProvider) || providerPending) { + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, + providerName); + values.put(LauncherSettings.Favorites.RESTORED, + LauncherAppWidgetInfo.RESTORE_COMPLETED); + String where = BaseColumns._ID + "= ?"; + String[] args = {Long.toString(id)}; + contentResolver.update(contentUri, values, where, args); + } } sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); sBgAppWidgets.add(appWidgetInfo); @@ -3605,6 +3634,11 @@ public class LauncherModel extends BroadcastReceiver } }; + static boolean isValidProvider(AppWidgetProviderInfo provider) { + return (provider != null) && (provider.provider != null) + && (provider.provider.getPackageName() != null); + } + public void dumpState() { Log.d(TAG, "mCallbacks=" + mCallbacks); AppInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mBgAllAppsList.data); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 267dde800..1e264588c 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -30,6 +30,8 @@ import android.app.WallpaperManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; @@ -47,6 +49,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.IBinder; import android.os.Parcelable; +import android.provider.BaseColumns; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; @@ -61,15 +64,16 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.TextView; -import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.Launcher.CustomContentCallbacks; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Set; /** * The workspace is a wide area with a wallpaper and a finite number of pages. @@ -4574,7 +4578,7 @@ public class Workspace extends SmoothPagedView public Folder getFolderForTag(final Object tag) { final Folder[] value = new Folder[1]; - mapOverShortcuts(MAP_NO_RECURSE, new ShortcutOperator() { + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { if (v instanceof Folder) { @@ -4592,7 +4596,7 @@ public class Workspace extends SmoothPagedView public View getViewForTag(final Object tag) { final View[] value = new View[1]; - mapOverShortcuts(MAP_NO_RECURSE, new ShortcutOperator() { + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { if (v.getTag() == tag) { @@ -4606,7 +4610,7 @@ public class Workspace extends SmoothPagedView } void clearDropTargets() { - mapOverShortcuts(MAP_NO_RECURSE, new ShortcutOperator() { + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { if (v instanceof DropTarget) { @@ -4760,9 +4764,9 @@ public class Workspace extends SmoothPagedView } } - interface ShortcutOperator { + interface ItemOperator { /** - * Process the next shortcut, possibly with side-effect on {@link ShortcutOperator#value}. + * Process the next itemInfo, possibly with side-effect on {@link ItemOperator#value}. * * @param info info for the shortcut * @param view view for the shortcut @@ -4773,12 +4777,12 @@ public class Workspace extends SmoothPagedView } /** - * Map the operator over the shortcuts, return the first-non-null value. + * Map the operator over the shortcuts and widgets, return the first-non-null value. * * @param recurse true: iterate over folder children. false: op get the folders themselves. * @param op the operator to map over the shortcuts */ - void mapOverShortcuts(boolean recurse, ShortcutOperator op) { + void mapOverItems(boolean recurse, ItemOperator op) { ArrayList containers = getAllShortcutAndWidgetContainers(); final int containerCount = containers.size(); for (int containerIdx = 0; containerIdx < containerCount; containerIdx++) { @@ -4809,14 +4813,16 @@ public class Workspace extends SmoothPagedView } } - void updateShortcuts(ArrayList apps) { + void updateShortcutsAndWidgets(ArrayList apps) { // Create a map of the apps to test against final HashMap appsMap = new HashMap(); + final HashSet pkgNames = new HashSet<>(); for (AppInfo ai : apps) { appsMap.put(ai.componentName, ai); + pkgNames.add(ai.componentName.getPackageName()); } - mapOverShortcuts(MAP_RECURSE, new ShortcutOperator() { + mapOverItems(MAP_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { if (info instanceof ShortcutInfo) { @@ -4829,6 +4835,8 @@ public class Workspace extends SmoothPagedView return false; } }); + + restorePendingWidgets(pkgNames); } public void removeAbandonedPromise(BubbleTextView abandonedIcon, UserHandleCompat user) { @@ -4844,7 +4852,7 @@ public class Workspace extends SmoothPagedView } public void updatePackageState(final String pkgName, final int state) { - mapOverShortcuts(MAP_RECURSE, new ShortcutOperator() { + mapOverItems(MAP_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { if (info instanceof ShortcutInfo @@ -4857,6 +4865,40 @@ public class Workspace extends SmoothPagedView return false; } }); + + if (state == ShortcutInfo.PACKAGE_STATE_DEFAULT) { + // Update any pending widget + HashSet packages = new HashSet<>(); + packages.add(pkgName); + restorePendingWidgets(packages); + } + } + + private void restorePendingWidgets(final Set installedPackaged) { + final ContentResolver contentResolver = getContext().getContentResolver(); + // Iterate non recursively as widgets can't be inside a folder. + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (info instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info; + if (widgetInfo.restoreStatus == LauncherAppWidgetInfo.RESTORE_PROVIDER_PENDING + && installedPackaged.contains(widgetInfo.providerName.getPackageName())) { + // Package installed. Update pending widgets. + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.RESTORED, + LauncherAppWidgetInfo.RESTORE_COMPLETED); + String where = BaseColumns._ID + "= ?"; + String[] args = {Long.toString(widgetInfo.id)}; + contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, + values, where, args); + } + } + // process all the widget + return false; + } + }); } private void moveToScreen(int page, boolean animate) { -- cgit v1.2.3