blob: 85f0e234241239cf3cd3c5ad256e36bbba61c5b1 [file] [log] [blame]
/*
* Copyright (C) 2008 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 android.appwidget;
import android.annotation.UnsupportedAppUsage;
import android.app.Activity;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.RemoteViews;
import android.widget.RemoteViews.OnClickHandler;
import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Provides the glue to show AppWidget views. This class offers automatic animation
* between updates, and will try recycling old views for each incoming
* {@link RemoteViews}.
*/
public class AppWidgetHostView extends FrameLayout {
static final String TAG = "AppWidgetHostView";
private static final String KEY_JAILED_ARRAY = "jail";
static final boolean LOGD = false;
static final int VIEW_MODE_NOINIT = 0;
static final int VIEW_MODE_CONTENT = 1;
static final int VIEW_MODE_ERROR = 2;
static final int VIEW_MODE_DEFAULT = 3;
// When we're inflating the initialLayout for a AppWidget, we only allow
// views that are allowed in RemoteViews.
private static final LayoutInflater.Filter INFLATER_FILTER =
(clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
Context mContext;
Context mRemoteContext;
@UnsupportedAppUsage
int mAppWidgetId;
@UnsupportedAppUsage
AppWidgetProviderInfo mInfo;
View mView;
int mViewMode = VIEW_MODE_NOINIT;
int mLayoutId = -1;
private OnClickHandler mOnClickHandler;
private boolean mOnLightBackground;
private Executor mAsyncExecutor;
private CancellationSignal mLastExecutionSignal;
/**
* Create a host view. Uses default fade animations.
*/
public AppWidgetHostView(Context context) {
this(context, android.R.anim.fade_in, android.R.anim.fade_out);
}
/**
* @hide
*/
public AppWidgetHostView(Context context, OnClickHandler handler) {
this(context, android.R.anim.fade_in, android.R.anim.fade_out);
mOnClickHandler = handler;
}
/**
* Create a host view. Uses specified animations when pushing
* {@link #updateAppWidget(RemoteViews)}.
*
* @param animationIn Resource ID of in animation to use
* @param animationOut Resource ID of out animation to use
*/
@SuppressWarnings({"UnusedDeclaration"})
public AppWidgetHostView(Context context, int animationIn, int animationOut) {
super(context);
mContext = context;
// We want to segregate the view ids within AppWidgets to prevent
// problems when those ids collide with view ids in the AppWidgetHost.
setIsRootNamespace(true);
}
/**
* Pass the given handler to RemoteViews when updating this widget. Unless this
* is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
* should be made.
* @param handler
* @hide
*/
public void setOnClickHandler(OnClickHandler handler) {
mOnClickHandler = handler;
}
/**
* Set the AppWidget that will be displayed by this view. This method also adds default padding
* to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)}
* and can be overridden in order to add custom padding.
*/
public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
mAppWidgetId = appWidgetId;
mInfo = info;
// We add padding to the AppWidgetHostView if necessary
Rect padding = getDefaultPadding();
setPadding(padding.left, padding.top, padding.right, padding.bottom);
// Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for
// a widget, eg. for some widgets in safe mode.
if (info != null) {
String description = info.loadLabel(getContext().getPackageManager());
if ((info.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) {
description = Resources.getSystem().getString(
com.android.internal.R.string.suspended_widget_accessibility, description);
}
setContentDescription(description);
}
}
/**
* As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting
* ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend
* that widget developers do not add extra padding to their widgets. This will help
* achieve consistency among widgets.
*
* Note: this method is only needed by developers of AppWidgetHosts. The method is provided in
* order for the AppWidgetHost to account for the automatic padding when computing the number
* of cells to allocate to a particular widget.
*
* @param context the current context
* @param component the component name of the widget
* @param padding Rect in which to place the output, if null, a new Rect will be allocated and
* returned
* @return default padding for this widget, in pixels
*/
public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
Rect padding) {
return getDefaultPaddingForWidget(context, padding);
}
private static Rect getDefaultPaddingForWidget(Context context, Rect padding) {
if (padding == null) {
padding = new Rect(0, 0, 0, 0);
} else {
padding.set(0, 0, 0, 0);
}
Resources r = context.getResources();
padding.left = r.getDimensionPixelSize(
com.android.internal.R.dimen.default_app_widget_padding_left);
padding.right = r.getDimensionPixelSize(
com.android.internal.R.dimen.default_app_widget_padding_right);
padding.top = r.getDimensionPixelSize(
com.android.internal.R.dimen.default_app_widget_padding_top);
padding.bottom = r.getDimensionPixelSize(
com.android.internal.R.dimen.default_app_widget_padding_bottom);
return padding;
}
private Rect getDefaultPadding() {
return getDefaultPaddingForWidget(mContext, null);
}
public int getAppWidgetId() {
return mAppWidgetId;
}
public AppWidgetProviderInfo getAppWidgetInfo() {
return mInfo;
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
final SparseArray<Parcelable> jail = new SparseArray<>();
super.dispatchSaveInstanceState(jail);
Bundle bundle = new Bundle();
bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail);
container.put(generateId(), bundle);
}
private int generateId() {
final int id = getId();
return id == View.NO_ID ? mAppWidgetId : id;
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
final Parcelable parcelable = container.get(generateId());
SparseArray<Parcelable> jail = null;
if (parcelable instanceof Bundle) {
jail = ((Bundle) parcelable).getSparseParcelableArray(KEY_JAILED_ARRAY);
}
if (jail == null) jail = new SparseArray<>();
try {
super.dispatchRestoreInstanceState(jail);
} catch (Exception e) {
Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
+ (mInfo == null ? "null" : mInfo.provider), e);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
try {
super.onLayout(changed, left, top, right, bottom);
} catch (final RuntimeException e) {
Log.e(TAG, "Remote provider threw runtime exception, using error view instead.", e);
removeViewInLayout(mView);
View child = getErrorView();
prepareView(child);
addViewInLayout(child, 0, child.getLayoutParams());
measureChild(child, MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
child.layout(0, 0, child.getMeasuredWidth() + mPaddingLeft + mPaddingRight,
child.getMeasuredHeight() + mPaddingTop + mPaddingBottom);
mView = child;
mViewMode = VIEW_MODE_ERROR;
}
}
/**
* Provide guidance about the size of this widget to the AppWidgetManager. The widths and
* heights should correspond to the full area the AppWidgetHostView is given. Padding added by
* the framework will be accounted for automatically. This information gets embedded into the
* AppWidget options and causes a callback to the AppWidgetProvider.
* @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
*
* @param newOptions The bundle of options, in addition to the size information,
* can be null.
* @param minWidth The minimum width in dips that the widget will be displayed at.
* @param minHeight The maximum height in dips that the widget will be displayed at.
* @param maxWidth The maximum width in dips that the widget will be displayed at.
* @param maxHeight The maximum height in dips that the widget will be displayed at.
*
*/
public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
int maxHeight) {
updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false);
}
/**
* @hide
*/
@UnsupportedAppUsage
public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
int maxHeight, boolean ignorePadding) {
if (newOptions == null) {
newOptions = new Bundle();
}
Rect padding = getDefaultPadding();
float density = getResources().getDisplayMetrics().density;
int xPaddingDips = (int) ((padding.left + padding.right) / density);
int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips);
int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips);
int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips);
int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips);
AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
// We get the old options to see if the sizes have changed
Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId);
boolean needsUpdate = false;
if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ||
newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) ||
newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ||
newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) {
needsUpdate = true;
}
if (needsUpdate) {
newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth);
newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight);
newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth);
newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight);
updateAppWidgetOptions(newOptions);
}
}
/**
* Specify some extra information for the widget provider. Causes a callback to the
* AppWidgetProvider.
* @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
*
* @param options The bundle of options information.
*/
public void updateAppWidgetOptions(Bundle options) {
AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
}
/** {@inheritDoc} */
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
// We're being asked to inflate parameters, probably by a LayoutInflater
// in a remote Context. To help resolve any remote references, we
// inflate through our last mRemoteContext when it exists.
final Context context = mRemoteContext != null ? mRemoteContext : mContext;
return new FrameLayout.LayoutParams(context, attrs);
}
/**
* Sets an executor which can be used for asynchronously inflating. CPU intensive tasks like
* view inflation or loading images will be performed on the executor. The updates will still
* be applied on the UI thread.
*
* @param executor the executor to use or null.
*/
public void setExecutor(Executor executor) {
if (mLastExecutionSignal != null) {
mLastExecutionSignal.cancel();
mLastExecutionSignal = null;
}
mAsyncExecutor = executor;
}
/**
* Sets whether the widget is being displayed on a light/white background and use an
* alternate UI if available.
* @see RemoteViews#setLightBackgroundLayoutId(int)
*/
public void setOnLightBackground(boolean onLightBackground) {
mOnLightBackground = onLightBackground;
}
/**
* Update the AppWidgetProviderInfo for this view, and reset it to the
* initial layout.
*/
void resetAppWidget(AppWidgetProviderInfo info) {
setAppWidget(mAppWidgetId, info);
mViewMode = VIEW_MODE_NOINIT;
updateAppWidget(null);
}
/**
* Process a set of {@link RemoteViews} coming in as an update from the
* AppWidget provider. Will animate into these new views as needed
*/
public void updateAppWidget(RemoteViews remoteViews) {
applyRemoteViews(remoteViews, true);
}
/**
* @hide
*/
protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) {
boolean recycled = false;
View content = null;
Exception exception = null;
if (mLastExecutionSignal != null) {
mLastExecutionSignal.cancel();
mLastExecutionSignal = null;
}
if (remoteViews == null) {
if (mViewMode == VIEW_MODE_DEFAULT) {
// We've already done this -- nothing to do.
return;
}
content = getDefaultView();
mLayoutId = -1;
mViewMode = VIEW_MODE_DEFAULT;
} else {
if (mOnLightBackground) {
remoteViews = remoteViews.getDarkTextViews();
}
if (mAsyncExecutor != null && useAsyncIfPossible) {
inflateAsync(remoteViews);
return;
}
// Prepare a local reference to the remote Context so we're ready to
// inflate any requested LayoutParams.
mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();
// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (content == null && layoutId == mLayoutId) {
try {
remoteViews.reapply(mContext, mView, mOnClickHandler);
content = mView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycle existing layout");
} catch (RuntimeException e) {
exception = e;
}
}
// Try normal RemoteView inflation
if (content == null) {
try {
content = remoteViews.apply(mContext, this, mOnClickHandler);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
mLayoutId = layoutId;
mViewMode = VIEW_MODE_CONTENT;
}
applyContent(content, recycled, exception);
}
private void applyContent(View content, boolean recycled, Exception exception) {
if (content == null) {
if (mViewMode == VIEW_MODE_ERROR) {
// We've already done this -- nothing to do.
return ;
}
if (exception != null) {
Log.w(TAG, "Error inflating RemoteViews : " + exception.toString());
}
content = getErrorView();
mViewMode = VIEW_MODE_ERROR;
}
if (!recycled) {
prepareView(content);
addView(content);
}
if (mView != content) {
removeView(mView);
mView = content;
}
}
private void inflateAsync(RemoteViews remoteViews) {
// Prepare a local reference to the remote Context so we're ready to
// inflate any requested LayoutParams.
mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();
// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (layoutId == mLayoutId && mView != null) {
try {
mLastExecutionSignal = remoteViews.reapplyAsync(mContext,
mView,
mAsyncExecutor,
new ViewApplyListener(remoteViews, layoutId, true),
mOnClickHandler);
} catch (Exception e) {
// Reapply failed. Try apply
}
}
if (mLastExecutionSignal == null) {
mLastExecutionSignal = remoteViews.applyAsync(mContext,
this,
mAsyncExecutor,
new ViewApplyListener(remoteViews, layoutId, false),
mOnClickHandler);
}
}
private class ViewApplyListener implements RemoteViews.OnViewAppliedListener {
private final RemoteViews mViews;
private final boolean mIsReapply;
private final int mLayoutId;
public ViewApplyListener(RemoteViews views, int layoutId, boolean isReapply) {
mViews = views;
mLayoutId = layoutId;
mIsReapply = isReapply;
}
@Override
public void onViewApplied(View v) {
AppWidgetHostView.this.mLayoutId = mLayoutId;
mViewMode = VIEW_MODE_CONTENT;
applyContent(v, mIsReapply, null);
}
@Override
public void onError(Exception e) {
if (mIsReapply) {
// Try a fresh replay
mLastExecutionSignal = mViews.applyAsync(mContext,
AppWidgetHostView.this,
mAsyncExecutor,
new ViewApplyListener(mViews, mLayoutId, false),
mOnClickHandler);
} else {
applyContent(null, false, e);
}
}
}
/**
* Process data-changed notifications for the specified view in the specified
* set of {@link RemoteViews} views.
*/
void viewDataChanged(int viewId) {
View v = findViewById(viewId);
if ((v != null) && (v instanceof AdapterView<?>)) {
AdapterView<?> adapterView = (AdapterView<?>) v;
Adapter adapter = adapterView.getAdapter();
if (adapter instanceof BaseAdapter) {
BaseAdapter baseAdapter = (BaseAdapter) adapter;
baseAdapter.notifyDataSetChanged();
} else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
// If the adapter is null, it may mean that the RemoteViewsAapter has not yet
// connected to its associated service, and hence the adapter hasn't been set.
// In this case, we need to defer the notify call until it has been set.
((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
}
}
}
/**
* Build a {@link Context} cloned into another package name, usually for the
* purposes of reading remote resources.
* @hide
*/
protected Context getRemoteContext() {
try {
// Return if cloned successfully, otherwise default
return mContext.createApplicationContext(
mInfo.providerInfo.applicationInfo,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(TAG, "Package name " + mInfo.providerInfo.packageName + " not found");
return mContext;
}
}
/**
* Prepare the given view to be shown. This might include adjusting
* {@link FrameLayout.LayoutParams} before inserting.
*/
protected void prepareView(View view) {
// Take requested dimensions from child, but apply default gravity.
FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
if (requested == null) {
requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
requested.gravity = Gravity.CENTER;
view.setLayoutParams(requested);
}
/**
* Inflate and return the default layout requested by AppWidget provider.
*/
protected View getDefaultView() {
if (LOGD) {
Log.d(TAG, "getDefaultView");
}
View defaultView = null;
Exception exception = null;
try {
if (mInfo != null) {
Context theirContext = getRemoteContext();
mRemoteContext = theirContext;
LayoutInflater inflater = (LayoutInflater)
theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(theirContext);
inflater.setFilter(INFLATER_FILTER);
AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
int layoutId = mInfo.initialLayout;
if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
int kgLayoutId = mInfo.initialKeyguardLayout;
// If a default keyguard layout is not specified, use the standard
// default layout.
layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
}
}
defaultView = inflater.inflate(layoutId, this, false);
defaultView.setOnClickListener(this::onDefaultViewClicked);
} else {
Log.w(TAG, "can't inflate defaultView because mInfo is missing");
}
} catch (RuntimeException e) {
exception = e;
}
if (exception != null) {
Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
}
if (defaultView == null) {
if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
defaultView = getErrorView();
}
return defaultView;
}
private void onDefaultViewClicked(View view) {
if (mInfo != null) {
LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class);
List<LauncherActivityInfo> activities = launcherApps.getActivityList(
mInfo.provider.getPackageName(), mInfo.getProfile());
if (!activities.isEmpty()) {
LauncherActivityInfo ai = activities.get(0);
launcherApps.startMainActivity(ai.getComponentName(), ai.getUser(),
RemoteViews.getSourceBounds(view), null);
}
}
}
/**
* Inflate and return a view that represents an error state.
*/
protected View getErrorView() {
TextView tv = new TextView(mContext);
tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
// TODO: get this color from somewhere.
tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
return tv;
}
/** @hide */
@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
info.setClassName(AppWidgetHostView.class.getName());
}
/** @hide */
public ActivityOptions createSharedElementActivityOptions(
int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) {
Context parentContext = getContext();
while ((parentContext instanceof ContextWrapper)
&& !(parentContext instanceof Activity)) {
parentContext = ((ContextWrapper) parentContext).getBaseContext();
}
if (!(parentContext instanceof Activity)) {
return null;
}
List<Pair<View, String>> sharedElements = new ArrayList<>();
Bundle extras = new Bundle();
for (int i = 0; i < sharedViewIds.length; i++) {
View view = findViewById(sharedViewIds[i]);
if (view != null) {
sharedElements.add(Pair.create(view, sharedViewNames[i]));
extras.putParcelable(sharedViewNames[i], RemoteViews.getSourceBounds(view));
}
}
if (!sharedElements.isEmpty()) {
fillInIntent.putExtra(RemoteViews.EXTRA_SHARED_ELEMENT_BOUNDS, extras);
final ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation(
(Activity) parentContext,
sharedElements.toArray(new Pair[sharedElements.size()]));
opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return opts;
}
return null;
}
}