summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk1
-rw-r--r--core/java/android/appwidget/AppWidgetManager.java40
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java647
-rw-r--r--core/java/com/android/internal/appwidget/IAppWidgetService.aidl8
-rw-r--r--core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl25
-rw-r--r--core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java380
-rw-r--r--services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java172
7 files changed, 713 insertions, 560 deletions
diff --git a/Android.mk b/Android.mk
index 6470e342f9d9..6f39e52e65a7 100644
--- a/Android.mk
+++ b/Android.mk
@@ -425,7 +425,6 @@ LOCAL_SRC_FILES += \
core/java/com/android/internal/widget/ICheckCredentialProgressCallback.aidl \
core/java/com/android/internal/widget/ILockSettings.aidl \
core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \
- core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl \
keystore/java/android/security/IKeyChainAliasCallback.aidl \
keystore/java/android/security/IKeyChainService.aidl \
location/java/android/location/IBatchedLocationCallback.aidl \
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 969b19ee48ac..37bb6b05c3e7 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -20,17 +20,19 @@ import android.annotation.BroadcastBehavior;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
-import android.annotation.SystemService;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.app.IServiceConnection;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.ServiceConnection;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.os.Bundle;
-import android.os.IBinder;
+import android.os.Handler;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -1051,43 +1053,23 @@ public class AppWidgetManager {
* The appWidgetId specified must already be bound to the calling AppWidgetHost via
* {@link android.appwidget.AppWidgetManager#bindAppWidgetId AppWidgetManager.bindAppWidgetId()}.
*
- * @param packageName The package from which the binding is requested.
* @param appWidgetId The AppWidget instance for which to bind the RemoteViewsService.
* @param intent The intent of the service which will be providing the data to the
* RemoteViewsAdapter.
* @param connection The callback interface to be notified when a connection is made or lost.
- * @hide
- */
- public void bindRemoteViewsService(String packageName, int appWidgetId, Intent intent,
- IBinder connection) {
- if (mService == null) {
- return;
- }
- try {
- mService.bindRemoteViewsService(packageName, appWidgetId, intent, connection);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Unbinds the RemoteViewsService for a given appWidgetId and intent.
- *
- * The appWidgetId specified muse already be bound to the calling AppWidgetHost via
- * {@link android.appwidget.AppWidgetManager#bindAppWidgetId AppWidgetManager.bindAppWidgetId()}.
+ * @param flags Flags used for binding to the service
*
- * @param packageName The package from which the binding is requested.
- * @param appWidgetId The AppWidget instance for which to bind the RemoteViewsService.
- * @param intent The intent of the service which will be providing the data to the
- * RemoteViewsAdapter.
+ * @see Context#getServiceDispatcher(ServiceConnection, Handler, int)
* @hide
*/
- public void unbindRemoteViewsService(String packageName, int appWidgetId, Intent intent) {
+ public boolean bindRemoteViewsService(Context context, int appWidgetId, Intent intent,
+ IServiceConnection connection, @Context.BindServiceFlags int flags) {
if (mService == null) {
- return;
+ return false;
}
try {
- mService.unbindRemoteViewsService(packageName, appWidgetId, intent);
+ return mService.bindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent,
+ context.getIApplicationThread(), context.getActivityToken(), connection, flags);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 51277e4316c8..e5ae0ca0070c 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -16,11 +16,14 @@
package android.widget;
-import android.Manifest;
+import android.annotation.WorkerThread;
+import android.app.IServiceConnection;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.os.Handler;
import android.os.HandlerThread;
@@ -29,7 +32,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
-import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
@@ -39,7 +41,6 @@ import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.RemoteViews.OnClickHandler;
-import com.android.internal.widget.IRemoteViewsAdapterConnection;
import com.android.internal.widget.IRemoteViewsFactory;
import java.lang.ref.WeakReference;
@@ -49,67 +50,68 @@ import java.util.LinkedList;
import java.util.concurrent.Executor;
/**
- * An adapter to a RemoteViewsService which fetches and caches RemoteViews
- * to be later inflated as child views.
+ * An adapter to a RemoteViewsService which fetches and caches RemoteViews to be later inflated as
+ * child views.
+ *
+ * The adapter runs in the host process, typically a Launcher app.
+ *
+ * It makes a service connection to the {@link RemoteViewsService} running in the
+ * AppWidgetsProvider's process. This connection is made on a background thread (and proxied via
+ * the platform to get the bind permissions) and all interaction with the service is done on the
+ * background thread.
+ *
+ * On first bind, the adapter will load can cache the RemoteViews locally. Afterwards the
+ * connection is only made when new RemoteViews are required.
+ * @hide
*/
-/** @hide */
public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback {
- private static final String MULTI_USER_PERM = Manifest.permission.INTERACT_ACROSS_USERS_FULL;
private static final String TAG = "RemoteViewsAdapter";
// The max number of items in the cache
- private static final int sDefaultCacheSize = 40;
+ private static final int DEFAULT_CACHE_SIZE = 40;
// The delay (in millis) to wait until attempting to unbind from a service after a request.
// This ensures that we don't stay continually bound to the service and that it can be destroyed
// if we need the memory elsewhere in the system.
- private static final int sUnbindServiceDelay = 5000;
+ private static final int UNBIND_SERVICE_DELAY = 5000;
// Default height for the default loading view, in case we cannot get inflate the first view
- private static final int sDefaultLoadingViewHeight = 50;
+ private static final int DEFAULT_LOADING_VIEW_HEIGHT = 50;
+
+ // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
+ // structures;
+ private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache>
+ sCachedRemoteViewsCaches = new HashMap<>();
+ private static final HashMap<RemoteViewsCacheKey, Runnable>
+ sRemoteViewsCacheRemoveRunnables = new HashMap<>();
+
+ private static HandlerThread sCacheRemovalThread;
+ private static Handler sCacheRemovalQueue;
- // Type defs for controlling different messages across the main and worker message queues
- private static final int sDefaultMessageType = 0;
- private static final int sUnbindServiceMessageType = 1;
+ // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation.
+ // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this
+ // duration, the cache is dropped.
+ private static final int REMOTE_VIEWS_CACHE_DURATION = 5000;
private final Context mContext;
private final Intent mIntent;
private final int mAppWidgetId;
private final Executor mAsyncViewLoadExecutor;
- private RemoteViewsAdapterServiceConnection mServiceConnection;
- private WeakReference<RemoteAdapterConnectionCallback> mCallback;
private OnClickHandler mRemoteViewsOnClickHandler;
private final FixedSizeRemoteViewsCache mCache;
private int mVisibleWindowLowerBound;
private int mVisibleWindowUpperBound;
- // A flag to determine whether we should notify data set changed after we connect
- private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
-
// The set of requested views that are to be notified when the associated RemoteViews are
// loaded.
private RemoteViewsFrameLayoutRefSet mRequestedViews;
- private HandlerThread mWorkerThread;
+ private final HandlerThread mWorkerThread;
// items may be interrupted within the normally processed queues
- private Handler mWorkerQueue;
- private Handler mMainQueue;
-
- // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
- // structures;
- private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache>
- sCachedRemoteViewsCaches = new HashMap<>();
- private static final HashMap<RemoteViewsCacheKey, Runnable>
- sRemoteViewsCacheRemoveRunnables = new HashMap<>();
-
- private static HandlerThread sCacheRemovalThread;
- private static Handler sCacheRemovalQueue;
-
- // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation.
- // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this
- // duration, the cache is dropped.
- private static final int REMOTE_VIEWS_CACHE_DURATION = 5000;
+ private final Handler mMainHandler;
+ private final RemoteServiceHandler mServiceHandler;
+ private final RemoteAdapterConnectionCallback mCallback;
// Used to indicate to the AdapterView that it can use this Adapter immediately after
// construction (happens when we have a cached FixedSizeRemoteViewsCache).
@@ -158,154 +160,192 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
}
+ static final int MSG_REQUEST_BIND = 1;
+ static final int MSG_NOTIFY_DATA_SET_CHANGED = 2;
+ static final int MSG_LOAD_NEXT_ITEM = 3;
+ static final int MSG_UNBIND_SERVICE = 4;
+
+ private static final int MSG_MAIN_HANDLER_COMMIT_METADATA = 1;
+ private static final int MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED = 2;
+ private static final int MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED = 3;
+ private static final int MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED = 4;
+ private static final int MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED = 5;
+
/**
- * The service connection that gets populated when the RemoteViewsService is
- * bound. This must be a static inner class to ensure that no references to the outer
- * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
- * garbage collected, and would cause us to leak activities due to the caching mechanism for
- * FrameLayouts in the adapter).
+ * Handler for various interactions with the {@link RemoteViewsService}.
*/
- private static class RemoteViewsAdapterServiceConnection extends
- IRemoteViewsAdapterConnection.Stub {
- private boolean mIsConnected;
- private boolean mIsConnecting;
- private WeakReference<RemoteViewsAdapter> mAdapter;
+ private static class RemoteServiceHandler extends Handler implements ServiceConnection {
+
+ private final WeakReference<RemoteViewsAdapter> mAdapter;
+ private final Context mContext;
+
private IRemoteViewsFactory mRemoteViewsFactory;
- public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
- mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
+ // The last call to notifyDataSetChanged didn't succeed, try again on next service bind.
+ private boolean mNotifyDataSetChangedPending = false;
+ private boolean mBindRequested = false;
+
+ RemoteServiceHandler(Looper workerLooper, RemoteViewsAdapter adapter, Context context) {
+ super(workerLooper);
+ mAdapter = new WeakReference<>(adapter);
+ mContext = context;
}
- public synchronized void bind(Context context, int appWidgetId, Intent intent) {
- if (!mIsConnecting) {
- try {
- RemoteViewsAdapter adapter;
- final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
- if ((adapter = mAdapter.get()) != null) {
- mgr.bindRemoteViewsService(context.getOpPackageName(), appWidgetId,
- intent, asBinder());
- } else {
- Slog.w(TAG, "bind: adapter was null");
- }
- mIsConnecting = true;
- } catch (Exception e) {
- Log.e("RVAServiceConnection", "bind(): " + e.getMessage());
- mIsConnecting = false;
- mIsConnected = false;
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // This is called on the same thread.
+ mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
+ enqueueDeferredUnbindServiceMessage();
+
+ RemoteViewsAdapter adapter = mAdapter.get();
+ if (adapter == null) {
+ return;
+ }
+
+ if (mNotifyDataSetChangedPending) {
+ mNotifyDataSetChangedPending = false;
+ Message msg = Message.obtain(this, MSG_NOTIFY_DATA_SET_CHANGED);
+ handleMessage(msg);
+ msg.recycle();
+ } else {
+ if (!sendNotifyDataSetChange(false)) {
+ return;
}
+
+ // Request meta data so that we have up to date data when calling back to
+ // the remote adapter callback
+ adapter.updateTemporaryMetaData(mRemoteViewsFactory);
+ adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_COMMIT_METADATA);
+ adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED);
}
}
- public synchronized void unbind(Context context, int appWidgetId, Intent intent) {
- try {
- RemoteViewsAdapter adapter;
- final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
- if ((adapter = mAdapter.get()) != null) {
- mgr.unbindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent);
- } else {
- Slog.w(TAG, "unbind: adapter was null");
- }
- mIsConnecting = false;
- } catch (Exception e) {
- Log.e("RVAServiceConnection", "unbind(): " + e.getMessage());
- mIsConnecting = false;
- mIsConnected = false;
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mRemoteViewsFactory = null;
+ RemoteViewsAdapter adapter = mAdapter.get();
+ if (adapter != null) {
+ adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED);
}
}
- public synchronized void onServiceConnected(IBinder service) {
- mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
+ @Override
+ public void handleMessage(Message msg) {
+ RemoteViewsAdapter adapter = mAdapter.get();
- // Remove any deferred unbind messages
- final RemoteViewsAdapter adapter = mAdapter.get();
- if (adapter == null) return;
-
- // Queue up work that we need to do for the callback to run
- adapter.mWorkerQueue.post(new Runnable() {
- @Override
- public void run() {
- if (adapter.mNotifyDataSetChangedAfterOnServiceConnected) {
- // Handle queued notifyDataSetChanged() if necessary
- adapter.onNotifyDataSetChanged();
- } else {
- IRemoteViewsFactory factory =
- adapter.mServiceConnection.getRemoteViewsFactory();
- try {
- if (!factory.isCreated()) {
- // We only call onDataSetChanged() if this is the factory was just
- // create in response to this bind
- factory.onDataSetChanged();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error notifying factory of data set changed in " +
- "onServiceConnected(): " + e.getMessage());
-
- // Return early to prevent anything further from being notified
- // (effectively nothing has changed)
- return;
- } catch (RuntimeException e) {
- Log.e(TAG, "Error notifying factory of data set changed in " +
- "onServiceConnected(): " + e.getMessage());
- }
+ switch (msg.what) {
+ case MSG_REQUEST_BIND: {
+ if (adapter == null || mRemoteViewsFactory != null) {
+ enqueueDeferredUnbindServiceMessage();
+ }
+ if (mBindRequested) {
+ return;
+ }
+ int flags = Context.BIND_AUTO_CREATE
+ | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE;
+ final IServiceConnection sd = mContext.getServiceDispatcher(this, this, flags);
+ Intent intent = (Intent) msg.obj;
+ int appWidgetId = msg.arg1;
+ mBindRequested = AppWidgetManager.getInstance(mContext)
+ .bindRemoteViewsService(mContext, appWidgetId, intent, sd, flags);
+ return;
+ }
+ case MSG_NOTIFY_DATA_SET_CHANGED: {
+ enqueueDeferredUnbindServiceMessage();
+ if (adapter == null) {
+ return;
+ }
+ if (mRemoteViewsFactory == null) {
+ mNotifyDataSetChangedPending = true;
+ adapter.requestBindService();
+ return;
+ }
+ if (!sendNotifyDataSetChange(true)) {
+ return;
+ }
- // Request meta data so that we have up to date data when calling back to
- // the remote adapter callback
- adapter.updateTemporaryMetaData();
-
- // Notify the host that we've connected
- adapter.mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- synchronized (adapter.mCache) {
- adapter.mCache.commitTemporaryMetaData();
- }
-
- final RemoteAdapterConnectionCallback callback =
- adapter.mCallback.get();
- if (callback != null) {
- callback.onRemoteAdapterConnected();
- }
- }
- });
+ // Flush the cache so that we can reload new items from the service
+ synchronized (adapter.mCache) {
+ adapter.mCache.reset();
}
- // Enqueue unbind message
- adapter.enqueueDeferredUnbindServiceMessage();
- mIsConnected = true;
- mIsConnecting = false;
- }
- });
- }
+ // Re-request the new metadata (only after the notification to the factory)
+ adapter.updateTemporaryMetaData(mRemoteViewsFactory);
+ int newCount;
+ int[] visibleWindow;
+ synchronized (adapter.mCache.getTemporaryMetaData()) {
+ newCount = adapter.mCache.getTemporaryMetaData().count;
+ visibleWindow = adapter.getVisibleWindow(newCount);
+ }
- public synchronized void onServiceDisconnected() {
- mIsConnected = false;
- mIsConnecting = false;
- mRemoteViewsFactory = null;
+ // Pre-load (our best guess of) the views which are currently visible in the
+ // AdapterView. This mitigates flashing and flickering of loading views when a
+ // widget notifies that its data has changed.
+ for (int position : visibleWindow) {
+ // Because temporary meta data is only ever modified from this thread
+ // (ie. mWorkerThread), it is safe to assume that count is a valid
+ // representation.
+ if (position < newCount) {
+ adapter.updateRemoteViews(mRemoteViewsFactory, position, false);
+ }
+ }
- // Clear the main/worker queues
- final RemoteViewsAdapter adapter = mAdapter.get();
- if (adapter == null) return;
+ // Propagate the notification back to the base adapter
+ adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_COMMIT_METADATA);
+ adapter.mMainHandler.sendEmptyMessage(
+ MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED);
+ return;
+ }
- adapter.mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- // Dequeue any unbind messages
- adapter.mMainQueue.removeMessages(sUnbindServiceMessageType);
+ case MSG_LOAD_NEXT_ITEM: {
+ if (adapter == null || mRemoteViewsFactory == null) {
+ return;
+ }
+ removeMessages(MSG_UNBIND_SERVICE);
+ // Get the next index to load
+ final int position = adapter.mCache.getNextIndexToLoad();
+ if (position > -1) {
+ // Load the item, and notify any existing RemoteViewsFrameLayouts
+ adapter.updateRemoteViews(mRemoteViewsFactory, position, true);
- final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
- if (callback != null) {
- callback.onRemoteAdapterDisconnected();
+ // Queue up for the next one to load
+ sendEmptyMessage(MSG_LOAD_NEXT_ITEM);
+ } else {
+ // No more items to load, so queue unbind
+ enqueueDeferredUnbindServiceMessage();
}
+ return;
+ }
+ case MSG_UNBIND_SERVICE: {
+ unbindNow();
+ return;
}
- });
+ }
}
- public synchronized IRemoteViewsFactory getRemoteViewsFactory() {
- return mRemoteViewsFactory;
+ protected void unbindNow() {
+ if (mBindRequested) {
+ mBindRequested = false;
+ mContext.unbindService(this);
+ }
+ mRemoteViewsFactory = null;
}
- public synchronized boolean isConnected() {
- return mIsConnected;
+ private boolean sendNotifyDataSetChange(boolean always) {
+ try {
+ if (always || !mRemoteViewsFactory.isCreated()) {
+ mRemoteViewsFactory.onDataSetChanged();
+ }
+ return true;
+ } catch (RemoteException | RuntimeException e) {
+ Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+ return false;
+ }
+ }
+
+ private void enqueueDeferredUnbindServiceMessage() {
+ removeMessages(MSG_UNBIND_SERVICE);
+ sendEmptyMessageDelayed(MSG_UNBIND_SERVICE, UNBIND_SERVICE_DELAY);
}
}
@@ -507,7 +547,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
*
*/
private static class FixedSizeRemoteViewsCache {
- private static final String TAG = "FixedSizeRemoteViewsCache";
// The meta data related to all the RemoteViews, ie. count, is stable, etc.
// The meta data objects are made final so that they can be locked on independently
@@ -671,7 +710,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
}
- int count = 0;
+ int count;
synchronized (mMetaData) {
count = mMetaData.count;
}
@@ -786,9 +825,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// Initialize the worker thread
mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
mWorkerThread.start();
- mWorkerQueue = new Handler(mWorkerThread.getLooper());
- mMainQueue = new Handler(Looper.myLooper(), this);
+ mMainHandler = new Handler(Looper.myLooper(), this);
+ mServiceHandler = new RemoteServiceHandler(mWorkerThread.getLooper(), this,
+ context.getApplicationContext());
mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null;
+ mCallback = callback;
if (sCacheRemovalThread == null) {
sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
@@ -796,10 +837,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper());
}
- // Initialize the cache and the service connection on startup
- mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
- mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
-
RemoteViewsCacheKey key = new RemoteViewsCacheKey(new Intent.FilterComparison(mIntent),
mAppWidgetId);
@@ -814,7 +851,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
}
} else {
- mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
+ mCache = new FixedSizeRemoteViewsCache(DEFAULT_CACHE_SIZE);
}
if (!mDataReady) {
requestBindService();
@@ -825,9 +862,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
@Override
protected void finalize() throws Throwable {
try {
- if (mWorkerThread != null) {
- mWorkerThread.quit();
- }
+ mServiceHandler.unbindNow();
+ mWorkerThread.quit();
} finally {
super.finalize();
}
@@ -864,16 +900,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
sCachedRemoteViewsCaches.put(key, mCache);
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- synchronized (sCachedRemoteViewsCaches) {
- if (sCachedRemoteViewsCaches.containsKey(key)) {
- sCachedRemoteViewsCaches.remove(key);
- }
- if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
- sRemoteViewsCacheRemoveRunnables.remove(key);
- }
+ Runnable r = () -> {
+ synchronized (sCachedRemoteViewsCaches) {
+ if (sCachedRemoteViewsCaches.containsKey(key)) {
+ sCachedRemoteViewsCaches.remove(key);
+ }
+ if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+ sRemoteViewsCacheRemoveRunnables.remove(key);
}
}
};
@@ -882,54 +915,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
}
- private void loadNextIndexInBackground() {
- mWorkerQueue.post(new Runnable() {
- @Override
- public void run() {
- if (mServiceConnection.isConnected()) {
- // Get the next index to load
- int position = -1;
- synchronized (mCache) {
- position = mCache.getNextIndexToLoad();
- }
- if (position > -1) {
- // Load the item, and notify any existing RemoteViewsFrameLayouts
- updateRemoteViews(position, true);
-
- // Queue up for the next one to load
- loadNextIndexInBackground();
- } else {
- // No more items to load, so queue unbind
- enqueueDeferredUnbindServiceMessage();
- }
- }
- }
- });
- }
-
- private void processException(String method, Exception e) {
- Log.e("RemoteViewsAdapter", "Error in " + method + ": " + e.getMessage());
-
- // If we encounter a crash when updating, we should reset the metadata & cache and trigger
- // a notifyDataSetChanged to update the widget accordingly
- final RemoteViewsMetaData metaData = mCache.getMetaData();
- synchronized (metaData) {
- metaData.reset();
- }
- synchronized (mCache) {
- mCache.reset();
- }
- mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- superNotifyDataSetChanged();
- }
- });
- }
-
- private void updateTemporaryMetaData() {
- IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
-
+ @WorkerThread
+ private void updateTemporaryMetaData(IRemoteViewsFactory factory) {
try {
// get the properties/first view (so that we can use it to
// measure our dummy views)
@@ -953,40 +940,40 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
tmpMetaData.count = count;
tmpMetaData.loadingTemplate = loadingTemplate;
}
- } catch(RemoteException e) {
- processException("updateMetaData", e);
- } catch(RuntimeException e) {
- processException("updateMetaData", e);
+ } catch (RemoteException | RuntimeException e) {
+ Log.e("RemoteViewsAdapter", "Error in updateMetaData: " + e.getMessage());
+
+ // If we encounter a crash when updating, we should reset the metadata & cache
+ // and trigger a notifyDataSetChanged to update the widget accordingly
+ synchronized (mCache.getMetaData()) {
+ mCache.getMetaData().reset();
+ }
+ synchronized (mCache) {
+ mCache.reset();
+ }
+ mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED);
}
}
- private void updateRemoteViews(final int position, boolean notifyWhenLoaded) {
- IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
-
+ @WorkerThread
+ private void updateRemoteViews(IRemoteViewsFactory factory, int position,
+ boolean notifyWhenLoaded) {
// Load the item information from the remote service
- RemoteViews remoteViews = null;
- long itemId = 0;
+ final RemoteViews remoteViews;
+ final long itemId;
try {
remoteViews = factory.getViewAt(position);
itemId = factory.getItemId(position);
- } catch (RemoteException e) {
+
+ if (remoteViews == null) {
+ throw new RuntimeException("Null remoteViews");
+ }
+ } catch (RemoteException | RuntimeException e) {
Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
// Return early to prevent additional work in re-centering the view cache, and
// swapping from the loading view
return;
- } catch (RuntimeException e) {
- Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
- return;
- }
-
- if (remoteViews == null) {
- // If a null view was returned, we break early to prevent it from getting
- // into our cache and causing problems later. The effect is that the child at this
- // position will remain as a loading view until it is updated.
- Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " +
- "returned from RemoteViewsFactory.");
- return;
}
if (remoteViews.mApplication != null) {
@@ -1013,21 +1000,15 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
synchronized (mCache) {
if (viewTypeInRange) {
- int[] visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
- mVisibleWindowUpperBound, cacheCount);
+ int[] visibleWindow = getVisibleWindow(cacheCount);
// Cache the RemoteViews we loaded
mCache.insert(position, remoteViews, itemId, visibleWindow);
- // Notify all the views that we have previously returned for this index that
- // there is new data for it.
- final RemoteViews rv = remoteViews;
if (notifyWhenLoaded) {
- mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- mRequestedViews.notifyOnRemoteViewsLoaded(position, rv);
- }
- });
+ // Notify all the views that we have previously returned for this index that
+ // there is new data for it.
+ Message.obtain(mMainHandler, MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED, position, 0,
+ remoteViews).sendToTarget();
}
} else {
// We need to log an error here, as the the view type count specified by the
@@ -1066,7 +1047,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
public int getItemViewType(int position) {
- int typeId = 0;
+ final int typeId;
synchronized (mCache) {
if (mCache.containsMetaDataAt(position)) {
typeId = mCache.getMetaDataAt(position).typeId;
@@ -1097,14 +1078,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
synchronized (mCache) {
RemoteViews rv = mCache.getRemoteViewsAt(position);
boolean isInCache = (rv != null);
- boolean isConnected = mServiceConnection.isConnected();
boolean hasNewItems = false;
if (convertView != null && convertView instanceof RemoteViewsFrameLayout) {
mRequestedViews.removeView((RemoteViewsFrameLayout) convertView);
}
- if (!isInCache && !isConnected) {
+ if (!isInCache) {
// Requesting bind service will trigger a super.notifyDataSetChanged(), which will
// in turn trigger another request to getView()
requestBindService();
@@ -1124,7 +1104,9 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
if (isInCache) {
// Apply the view synchronously if possible, to avoid flickering
layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false);
- if (hasNewItems) loadNextIndexInBackground();
+ if (hasNewItems) {
+ mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM);
+ }
} else {
// If the views is not loaded, apply the loading view. If the loading view doesn't
// exist, the layout will create a default view based on the firstView height.
@@ -1134,7 +1116,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
false);
mRequestedViews.add(position, layout);
mCache.queueRequestedPositionToLoad(position);
- loadNextIndexInBackground();
+ mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM);
}
return layout;
}
@@ -1158,69 +1140,12 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
return getCount() <= 0;
}
- private void onNotifyDataSetChanged() {
- // Complete the actual notifyDataSetChanged() call initiated earlier
- IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
- try {
- factory.onDataSetChanged();
- } catch (RemoteException e) {
- Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
-
- // Return early to prevent from further being notified (since nothing has
- // changed)
- return;
- } catch (RuntimeException e) {
- Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
- return;
- }
-
- // Flush the cache so that we can reload new items from the service
- synchronized (mCache) {
- mCache.reset();
- }
-
- // Re-request the new metadata (only after the notification to the factory)
- updateTemporaryMetaData();
- int newCount;
- int[] visibleWindow;
- synchronized(mCache.getTemporaryMetaData()) {
- newCount = mCache.getTemporaryMetaData().count;
- visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
- mVisibleWindowUpperBound, newCount);
- }
-
- // Pre-load (our best guess of) the views which are currently visible in the AdapterView.
- // This mitigates flashing and flickering of loading views when a widget notifies that
- // its data has changed.
- for (int i: visibleWindow) {
- // Because temporary meta data is only ever modified from this thread (ie.
- // mWorkerThread), it is safe to assume that count is a valid representation.
- if (i < newCount) {
- updateRemoteViews(i, false);
- }
- }
-
- // Propagate the notification back to the base adapter
- mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mCache) {
- mCache.commitTemporaryMetaData();
- }
-
- superNotifyDataSetChanged();
- enqueueDeferredUnbindServiceMessage();
- }
- });
-
- // Reset the notify flagflag
- mNotifyDataSetChangedAfterOnServiceConnected = false;
- }
-
/**
* Returns a sorted array of all integers between lower and upper.
*/
- private int[] getVisibleWindow(int lower, int upper, int count) {
+ private int[] getVisibleWindow(int count) {
+ int lower = mVisibleWindowLowerBound;
+ int upper = mVisibleWindowUpperBound;
// In the case that the window is invalid or uninitialized, return an empty window.
if ((lower == 0 && upper == 0) || lower < 0 || upper < 0) {
return new int[0];
@@ -1250,23 +1175,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
public void notifyDataSetChanged() {
- // Dequeue any unbind messages
- mMainQueue.removeMessages(sUnbindServiceMessageType);
-
- // If we are not connected, queue up the notifyDataSetChanged to be handled when we do
- // connect
- if (!mServiceConnection.isConnected()) {
- mNotifyDataSetChangedAfterOnServiceConnected = true;
- requestBindService();
- return;
- }
-
- mWorkerQueue.post(new Runnable() {
- @Override
- public void run() {
- onNotifyDataSetChanged();
- }
- });
+ mServiceHandler.removeMessages(MSG_UNBIND_SERVICE);
+ mServiceHandler.sendEmptyMessage(MSG_NOTIFY_DATA_SET_CHANGED);
}
void superNotifyDataSetChanged() {
@@ -1275,35 +1185,38 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
@Override
public boolean handleMessage(Message msg) {
- boolean result = false;
switch (msg.what) {
- case sUnbindServiceMessageType:
- if (mServiceConnection.isConnected()) {
- mServiceConnection.unbind(mContext, mAppWidgetId, mIntent);
+ case MSG_MAIN_HANDLER_COMMIT_METADATA: {
+ mCache.commitTemporaryMetaData();
+ return true;
+ }
+ case MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED: {
+ superNotifyDataSetChanged();
+ return true;
+ }
+ case MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED: {
+ if (mCallback != null) {
+ mCallback.onRemoteAdapterConnected();
+ }
+ return true;
+ }
+ case MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED: {
+ if (mCallback != null) {
+ mCallback.onRemoteAdapterDisconnected();
+ }
+ return true;
+ }
+ case MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED: {
+ mRequestedViews.notifyOnRemoteViewsLoaded(msg.arg1, (RemoteViews) msg.obj);
+ return true;
}
- result = true;
- break;
- default:
- break;
}
- return result;
- }
-
- private void enqueueDeferredUnbindServiceMessage() {
- // Remove any existing deferred-unbind messages
- mMainQueue.removeMessages(sUnbindServiceMessageType);
- mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
+ return false;
}
- private boolean requestBindService() {
- // Try binding the service (which will start it if it's not already running)
- if (!mServiceConnection.isConnected()) {
- mServiceConnection.bind(mContext, mAppWidgetId, mIntent);
- }
-
- // Remove any existing deferred-unbind messages
- mMainQueue.removeMessages(sUnbindServiceMessageType);
- return mServiceConnection.isConnected();
+ private void requestBindService() {
+ mServiceHandler.removeMessages(MSG_UNBIND_SERVICE);
+ Message.obtain(mServiceHandler, MSG_REQUEST_BIND, mAppWidgetId, 0, mIntent).sendToTarget();
}
private static class HandlerThreadExecutor implements Executor {
@@ -1331,7 +1244,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
remoteViews = views;
float density = context.getResources().getDisplayMetrics().density;
- defaultHeight = Math.round(sDefaultLoadingViewHeight * density);
+ defaultHeight = Math.round(DEFAULT_LOADING_VIEW_HEIGHT * density);
}
public void loadFirstViewHeight(
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index caf35b39b324..a4da6b9cea18 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -26,6 +26,8 @@ import com.android.internal.appwidget.IAppWidgetHost;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.RemoteViews;
+import android.app.IApplicationThread;
+import android.app.IServiceConnection;
/** {@hide} */
interface IAppWidgetService {
@@ -62,9 +64,9 @@ interface IAppWidgetService {
void setBindAppWidgetPermission(in String packageName, int userId, in boolean permission);
boolean bindAppWidgetId(in String callingPackage, int appWidgetId,
int providerProfileId, in ComponentName providerComponent, in Bundle options);
- void bindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent,
- in IBinder connection);
- void unbindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent);
+ boolean bindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent,
+ IApplicationThread caller, IBinder token, IServiceConnection connection, int flags);
+
int[] getAppWidgetIds(in ComponentName providerComponent);
boolean isBoundWidgetPackage(String packageName, int userId);
boolean requestPinAppWidget(String packageName, in ComponentName providerComponent,
diff --git a/core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl b/core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl
deleted file mode 100644
index 7294124b4cdc..000000000000
--- a/core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2011 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.internal.widget;
-
-import android.os.IBinder;
-
-/** {@hide} */
-oneway interface IRemoteViewsAdapterConnection {
- void onServiceConnected(IBinder service);
- void onServiceDisconnected();
-}
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java b/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java
new file mode 100644
index 000000000000..06b860a96117
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2017 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.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.IServiceConnection;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+
+import com.android.frameworks.coretests.R;
+import com.android.internal.widget.IRemoteViewsFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * Tests for RemoteViewsAdapter.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RemoteViewsAdapterTest {
+
+ @Mock AppWidgetManager mAppWidgetManager;
+ @Mock IServiceConnection mIServiceConnection;
+ @Mock RemoteViewsAdapter.RemoteAdapterConnectionCallback mCallback;
+
+ private Handler mMainHandler;
+ private TestContext mContext;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mAppWidgetManager
+ .bindRemoteViewsService(any(), anyInt(), any(), any(), anyInt())).thenReturn(true);
+ mContext = new TestContext();
+ mMainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Test
+ public void onRemoteAdapterConnected_after_metadata_loaded() throws Throwable {
+ RemoteViewsAdapter adapter = getOnUiThread(
+ () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false));
+ assertFalse(adapter.isDataReady());
+
+ assertNotNull(mContext.conn.get());
+
+ ViewsFactory factory = new ViewsFactory(1);
+ mContext.sendConnect(factory);
+
+ waitOnHandler(mMainHandler);
+ verify(mCallback, never()).onRemoteAdapterConnected();
+
+ factory.loadingView.set(createViews("loading"));
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+ verify(mCallback, times(1)).onRemoteAdapterConnected();
+
+ assertEquals((Integer) 1, getOnUiThread(adapter::getCount));
+
+ // Service is unbound
+ assertTrue(isUnboundOrScheduled());
+ }
+
+ @Test
+ public void viewReplaced_after_mainView_loaded() throws Throwable {
+ RemoteViewsAdapter adapter = getOnUiThread(
+ () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false));
+
+ ViewsFactory factory = new ViewsFactory(1);
+ factory.loadingView.set(createViews("loading"));
+ mContext.sendConnect(factory);
+
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+
+ // Returned view contains the loading text
+ View view = getOnUiThread(() -> adapter.getView(0, null, new FrameLayout(mContext)));
+ ArrayList<View> search = new ArrayList<>();
+ view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT);
+ assertEquals(1, search.size());
+
+ // Send the final remoteViews
+ factory.views[0].set(createViews("updated"));
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+
+ // Existing view got updated with new text
+ search.clear();
+ view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT);
+ assertTrue(search.isEmpty());
+ view.findViewsWithText(search, "updated", View.FIND_VIEWS_WITH_TEXT);
+ assertEquals(1, search.size());
+
+ // Service is unbound
+ assertTrue(isUnboundOrScheduled());
+ }
+
+ @Test
+ public void notifyDataSetChanged_deferred() throws Throwable {
+ RemoteViewsAdapter adapter = getOnUiThread(
+ () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false));
+
+ ViewsFactory factory = new ViewsFactory(1);
+ factory.loadingView.set(createViews("loading"));
+ mContext.sendConnect(factory);
+
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+ assertEquals((Integer) 1, getOnUiThread(adapter::getCount));
+
+ // Reset the loading view so that next refresh is blocked
+ factory.loadingView = new LockedValue<>();
+ factory.mCount = 3;
+ DataSetObserver observer = mock(DataSetObserver.class);
+ getOnUiThread(() -> {
+ adapter.registerDataSetObserver(observer);
+ adapter.notifyDataSetChanged();
+ return null;
+ });
+
+ waitOnHandler(mMainHandler);
+ // Still giving the old values
+ verify(observer, never()).onChanged();
+ assertEquals((Integer) 1, getOnUiThread(adapter::getCount));
+
+ factory.loadingView.set(createViews("refreshed"));
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+
+ // When the service returns new data, UI is updated.
+ verify(observer, times(1)).onChanged();
+ assertEquals((Integer) 3, getOnUiThread(adapter::getCount));
+
+ // Service is unbound
+ assertTrue(isUnboundOrScheduled());
+ }
+
+ @Test
+ public void serviceDisconnected_before_getView() throws Throwable {
+ RemoteViewsAdapter adapter = getOnUiThread(
+ () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false));
+
+ ViewsFactory factory = new ViewsFactory(1);
+ factory.loadingView.set(createViews("loading"));
+ mContext.sendConnect(factory);
+
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+ verify(mCallback, times(1)).onRemoteAdapterConnected();
+ assertEquals((Integer) 1, getOnUiThread(adapter::getCount));
+
+ // Unbind the service
+ ServiceConnection conn = mContext.conn.get();
+ getOnHandler(mContext.handler.get(), () -> {
+ conn.onServiceDisconnected(null);
+ return null;
+ });
+
+ // Returned view contains the loading text
+ View view = getOnUiThread(() -> adapter.getView(0, null, new FrameLayout(mContext)));
+ ArrayList<View> search = new ArrayList<>();
+ view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT);
+ assertEquals(1, search.size());
+
+ // Unbind is not scheduled
+ assertFalse(isUnboundOrScheduled());
+
+ mContext.sendConnect(factory);
+ waitOnHandler(mContext.handler.get());
+ waitOnHandler(mMainHandler);
+ verify(mCallback, times(2)).onRemoteAdapterConnected();
+ }
+
+ private RemoteViews createViews(String text) {
+ RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.remote_views_text);
+ views.setTextViewText(R.id.text, text);
+ return views;
+ }
+
+ private <T> T getOnUiThread(Supplier<T> supplier) throws Throwable {
+ return getOnHandler(mMainHandler, supplier);
+ }
+
+ private boolean isUnboundOrScheduled() throws Throwable {
+ Handler handler = mContext.handler.get();
+ return getOnHandler(handler, () -> mContext.boundCount == 0
+ || handler.hasMessages(RemoteViewsAdapter.MSG_UNBIND_SERVICE));
+ }
+
+ private static <T> T getOnHandler(Handler handler, Supplier<T> supplier) throws Throwable {
+ LockedValue<T> result = new LockedValue<>();
+ handler.post(() -> result.set(supplier.get()));
+ return result.get();
+ }
+
+ private class TestContext extends ContextWrapper {
+
+ public final LockedValue<ServiceConnection> conn = new LockedValue<>();
+ public final LockedValue<Handler> handler = new LockedValue<>();
+ public int boundCount;
+
+ TestContext() {
+ super(InstrumentationRegistry.getContext());
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ boundCount--;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.APPWIDGET_SERVICE.equals(name)) {
+ return mAppWidgetManager;
+ }
+ return super.getSystemService(name);
+ }
+
+ @Override
+ public IServiceConnection getServiceDispatcher(
+ ServiceConnection conn, Handler handler, int flags) {
+ this.conn.set(conn);
+ this.handler.set(handler);
+ boundCount++;
+ return mIServiceConnection;
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+
+ public void sendConnect(ViewsFactory factory) throws Exception {
+ ServiceConnection connection = conn.get();
+ handler.get().post(() -> connection.onServiceConnected(null, factory.asBinder()));
+ }
+ }
+
+ private static void waitOnHandler(Handler handler) throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ handler.post(() -> latch.countDown());
+ latch.await(20, TimeUnit.SECONDS);
+ }
+
+ private static class ViewsFactory extends IRemoteViewsFactory.Stub {
+
+ public LockedValue<RemoteViews> loadingView = new LockedValue<>();
+ public LockedValue<RemoteViews>[] views;
+
+ private int mCount;
+
+ ViewsFactory(int count) {
+ mCount = count;
+ views = new LockedValue[count];
+ for (int i = 0; i < count; i++) {
+ views[i] = new LockedValue<>();
+ }
+ }
+
+ @Override
+ public void onDataSetChanged() {}
+
+ @Override
+ public void onDataSetChangedAsync() { }
+
+ @Override
+ public void onDestroy(Intent intent) { }
+
+ @Override
+ public int getCount() throws RemoteException {
+ return mCount;
+ }
+
+ @Override
+ public RemoteViews getViewAt(int position) throws RemoteException {
+ try {
+ return views[position].get();
+ } catch (Exception e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ @Override
+ public RemoteViews getLoadingView() throws RemoteException {
+ try {
+ return loadingView.get();
+ } catch (Exception e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isCreated() {
+ return false;
+ }
+ }
+
+ private static class DistinctIntent extends Intent {
+
+ @Override
+ public boolean filterEquals(Intent other) {
+ return false;
+ }
+ }
+
+ private static class LockedValue<T> {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private T mValue;
+
+ public void set(T value) {
+ mValue = value;
+ mLatch.countDown();
+ }
+
+ public T get() throws Exception {
+ mLatch.await(10, TimeUnit.SECONDS);
+ return mValue;
+ }
+ }
+}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 51afada2a90a..76e77825b227 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -21,9 +21,12 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.IApplicationThread;
+import android.app.IServiceConnection;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManagerInternal;
@@ -99,7 +102,6 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
-import com.android.internal.widget.IRemoteViewsAdapterConnection;
import com.android.internal.widget.IRemoteViewsFactory;
import com.android.server.LocalServices;
import com.android.server.WidgetBackupProvider;
@@ -191,10 +193,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
};
- // Manages active connections to RemoteViewsServices.
- private final HashMap<Pair<Integer, FilterComparison>, ServiceConnection>
- mBoundRemoteViewsServices = new HashMap<>();
-
// Manages persistent references to RemoteViewsServices from different App Widgets.
private final HashMap<Pair<Integer, FilterComparison>, HashSet<Integer>>
mRemoteViewsServicesAppWidgets = new HashMap<>();
@@ -1209,17 +1207,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
@Override
- public void bindRemoteViewsService(String callingPackage, int appWidgetId,
- Intent intent, IBinder callbacks) {
+ public boolean bindRemoteViewsService(String callingPackage, int appWidgetId, Intent intent,
+ IApplicationThread caller, IBinder activtiyToken, IServiceConnection connection,
+ int flags) {
final int userId = UserHandle.getCallingUserId();
-
if (DEBUG) {
Slog.i(TAG, "bindRemoteViewsService() " + userId);
}
- // Make sure the package runs under the caller uid.
- mSecurityPolicy.enforceCallFromPackage(callingPackage);
-
synchronized (mLock) {
ensureGroupStateLoadedLocked(userId);
@@ -1254,76 +1249,35 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
mSecurityPolicy.enforceServiceExistsAndRequiresBindRemoteViewsPermission(
componentName, widget.provider.getUserId());
- // Good to go - the service pakcage is correct, it exists for the correct
+ // Good to go - the service package is correct, it exists for the correct
// user, and requires the bind permission.
- // If there is already a connection made for this service intent, then
- // disconnect from that first. (This does not allow multiple connections
- // to the same service under the same key).
- ServiceConnectionProxy connection = null;
- FilterComparison fc = new FilterComparison(intent);
- Pair<Integer, FilterComparison> key = Pair.create(appWidgetId, fc);
-
- if (mBoundRemoteViewsServices.containsKey(key)) {
- connection = (ServiceConnectionProxy) mBoundRemoteViewsServices.get(key);
- connection.disconnect();
- unbindService(connection);
- mBoundRemoteViewsServices.remove(key);
- }
-
- // Bind to the RemoteViewsService (which will trigger a callback to the
- // RemoteViewsAdapter.onServiceConnected())
- connection = new ServiceConnectionProxy(callbacks);
- bindService(intent, connection, widget.provider.info.getProfile());
- mBoundRemoteViewsServices.put(key, connection);
-
- // Add it to the mapping of RemoteViewsService to appWidgetIds so that we
- // can determine when we can call back to the RemoteViewsService later to
- // destroy associated factories.
- Pair<Integer, FilterComparison> serviceId = Pair.create(widget.provider.id.uid, fc);
- incrementAppWidgetServiceRefCount(appWidgetId, serviceId);
- }
- }
-
- @Override
- public void unbindRemoteViewsService(String callingPackage, int appWidgetId, Intent intent) {
- final int userId = UserHandle.getCallingUserId();
-
- if (DEBUG) {
- Slog.i(TAG, "unbindRemoteViewsService() " + userId);
- }
-
- // Make sure the package runs under the caller uid.
- mSecurityPolicy.enforceCallFromPackage(callingPackage);
-
- synchronized (mLock) {
- ensureGroupStateLoadedLocked(userId);
-
- // Unbind from the RemoteViewsService (which will trigger a callback to the bound
- // RemoteViewsAdapter)
- Pair<Integer, FilterComparison> key = Pair.create(appWidgetId,
- new FilterComparison(intent));
- if (mBoundRemoteViewsServices.containsKey(key)) {
- // We don't need to use the appWidgetId until after we are sure there is something
- // to unbind. Note that this may mask certain issues with apps calling unbind()
- // more than necessary.
-
- // NOTE: The lookup is enforcing security across users by making
- // sure the caller can only access widgets it hosts or provides.
- Widget widget = lookupWidgetLocked(appWidgetId,
- Binder.getCallingUid(), callingPackage);
-
- if (widget == null) {
- throw new IllegalArgumentException("Bad widget id " + appWidgetId);
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ // Ask ActivityManager to bind it. Notice that we are binding the service with the
+ // caller app instead of DevicePolicyManagerService.
+ if(ActivityManager.getService().bindService(
+ caller, activtiyToken, intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ connection, flags, mContext.getOpPackageName(),
+ widget.provider.getUserId()) != 0) {
+
+ // Add it to the mapping of RemoteViewsService to appWidgetIds so that we
+ // can determine when we can call back to the RemoteViewsService later to
+ // destroy associated factories.
+ incrementAppWidgetServiceRefCount(appWidgetId,
+ Pair.create(widget.provider.id.uid, new FilterComparison(intent)));
+ return true;
}
-
- ServiceConnectionProxy connection = (ServiceConnectionProxy)
- mBoundRemoteViewsServices.get(key);
- connection.disconnect();
- mContext.unbindService(connection);
- mBoundRemoteViewsServices.remove(key);
+ } catch (RemoteException ex) {
+ // Same process, should not happen.
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
}
}
+
+ // Failed to bind.
+ return false;
}
@Override
@@ -1754,7 +1708,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
private void deleteAppWidgetLocked(Widget widget) {
// We first unbind all services that are bound to this id
- unbindAppWidgetRemoteViewsServicesLocked(widget);
+ // Check if we need to destroy any services (if no other app widgets are
+ // referencing the same service)
+ decrementAppWidgetServiceRefCount(widget);
Host host = widget.host;
host.widgets.remove(widget);
@@ -1796,28 +1752,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
}
- // Unbinds from a RemoteViewsService when we delete an app widget
- private void unbindAppWidgetRemoteViewsServicesLocked(Widget widget) {
- int appWidgetId = widget.appWidgetId;
- // Unbind all connections to Services bound to this AppWidgetId
- Iterator<Pair<Integer, Intent.FilterComparison>> it = mBoundRemoteViewsServices.keySet()
- .iterator();
- while (it.hasNext()) {
- final Pair<Integer, Intent.FilterComparison> key = it.next();
- if (key.first == appWidgetId) {
- final ServiceConnectionProxy conn = (ServiceConnectionProxy)
- mBoundRemoteViewsServices.get(key);
- conn.disconnect();
- mContext.unbindService(conn);
- it.remove();
- }
- }
-
- // Check if we need to destroy any services (if no other app widgets are
- // referencing the same service)
- decrementAppWidgetServiceRefCount(widget);
- }
-
// Destroys the cached factory on the RemoteViewsService's side related to the specified intent
private void destroyRemoteViewsService(final Intent intent, Widget widget) {
final ServiceConnection conn = new ServiceConnection() {
@@ -1853,7 +1787,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
// Adds to the ref-count for a given RemoteViewsService intent
private void incrementAppWidgetServiceRefCount(int appWidgetId,
Pair<Integer, FilterComparison> serviceId) {
- HashSet<Integer> appWidgetIds = null;
+ final HashSet<Integer> appWidgetIds;
if (mRemoteViewsServicesAppWidgets.containsKey(serviceId)) {
appWidgetIds = mRemoteViewsServicesAppWidgets.get(serviceId);
} else {
@@ -4055,40 +3989,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
}
- /**
- * Acts as a proxy between the ServiceConnection and the RemoteViewsAdapterConnection. This
- * needs to be a static inner class since a reference to the ServiceConnection is held globally
- * and may lead us to leak AppWidgetService instances (if there were more than one).
- */
- private static final class ServiceConnectionProxy implements ServiceConnection {
- private final IRemoteViewsAdapterConnection mConnectionCb;
-
- ServiceConnectionProxy(IBinder connectionCb) {
- mConnectionCb = IRemoteViewsAdapterConnection.Stub
- .asInterface(connectionCb);
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- try {
- mConnectionCb.onServiceConnected(service);
- } catch (RemoteException re) {
- Slog.e(TAG, "Error passing service interface", re);
- }
- }
-
- public void onServiceDisconnected(ComponentName name) {
- disconnect();
- }
-
- public void disconnect() {
- try {
- mConnectionCb.onServiceDisconnected();
- } catch (RemoteException re) {
- Slog.e(TAG, "Error clearing service interface", re);
- }
- }
- }
-
private class LoadedWidgetState {
final Widget widget;
final int hostTag;
@@ -4642,7 +4542,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
// reconstructed due to the restore
host.widgets.remove(widget);
provider.widgets.remove(widget);
- unbindAppWidgetRemoteViewsServicesLocked(widget);
+ // Check if we need to destroy any services (if no other app widgets are
+ // referencing the same service)
+ decrementAppWidgetServiceRefCount(widget);
removeWidgetLocked(widget);
}
}