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
This commit is contained in:
Sunny Goyal
2014-07-21 10:34:21 -07:00
parent 82cc3fe6c7
commit 651077bdd6
12 changed files with 326 additions and 39 deletions
+7
View File
@@ -188,6 +188,13 @@
</intent-filter>
</receiver>
<!-- Intent received used to initialize a restored widget -->
<receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED"/>
</intent-filter>
</receiver>
<!-- New user initialization; set up initial wallpaper -->
<receiver
android:name="com.android.launcher3.UserInitializeReceiver"
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<corners android:radius="5dp" />
<solid android:color="@android:color/white" />
</shape>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dip"
android:paddingRight="20dip"
android:layout_margin="10dip"
android:gravity="center"
android:background="@drawable/bg_appwidget_not_ready"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:textColor="@color/appwidget_not_ready_color"
android:text="@string/gadget_pending_text"
/>
+1
View File
@@ -26,6 +26,7 @@
<color name="bubble_dark_background">#20000000</color>
<color name="appwidget_error_color">#FCCC</color>
<color name="appwidget_not_ready_color">#F48F</color>
<color name="workspace_all_apps_and_delete_zone_text_color">#CCFFFFFF</color>
<color name="workspace_all_apps_and_delete_zone_text_shadow_color">#A0000000</color>
+3
View File
@@ -190,6 +190,9 @@ s -->
<!-- Text to show user in place of a gadget when we can't display it properly -->
<string name="gadget_error_text">Problem loading widget</string>
<!-- Text to show user in place of a gadget when it is not yet ready/initialized. -->
<string name="gadget_pending_text" translatable="false">Widget not ready</string>
<!-- Text to inform the user that they can't uninstall a system application -->
<string name="uninstall_system_app_text">This is a system app and can\'t be uninstalled.</string>
@@ -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<Integer> 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<Void, Void, Void>() {
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);
}
}
}
@@ -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<Void, Void, Void>() {
+17 -7
View File
@@ -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() &&
@@ -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
@@ -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;
}
}
+54 -20
View File
@@ -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);
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);
if (!isSafeMode && (provider == null || provider.provider == null ||
provider.provider.getPackageName() == null)) {
String log = "Deleting widget that isn't installed anymore: id="
+ id + " appWidgetId=" + appWidgetId;
// 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);
+53 -11
View File
@@ -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<ShortcutAndWidgetContainer> 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<AppInfo> apps) {
void updateShortcutsAndWidgets(ArrayList<AppInfo> apps) {
// Create a map of the apps to test against
final HashMap<ComponentName, AppInfo> appsMap = new HashMap<ComponentName, AppInfo>();
final HashSet<String> 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<String> packages = new HashSet<>();
packages.add(pkgName);
restorePendingWidgets(packages);
}
}
private void restorePendingWidgets(final Set<String> 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) {