Binding to the RemoteViewsService directly from the host

The host passes an IServiceConnection to the AppWidgetManager which
is used to bind to the RemoteViewsService. This allows the host to
recieve the connection callbacks directly instead of proxying it via
the AppWidgetManager. The host is also responsible for unbinding to
the service.

Bug: 26481160
Test: adb shell \
  am instrument -w -e class android.widget.RemoteViewsAdapterTest \
  com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner

Change-Id: Iac400095a319c3a43714c82fda7516be1ccc68af
diff --git a/Android.mk b/Android.mk
index 6470e34..6f39e52 100644
--- a/Android.mk
+++ b/Android.mk
@@ -425,7 +425,6 @@
 	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 969b19e..37bb6b0 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -20,17 +20,19 @@
 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 @@
      * 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.
+     * @param flags         Flags used for binding to the service
+     *
+     * @see Context#getServiceDispatcher(ServiceConnection, Handler, int)
      * @hide
      */
-    public void bindRemoteViewsService(String packageName, int appWidgetId, Intent intent,
-            IBinder connection) {
+    public boolean bindRemoteViewsService(Context context, int appWidgetId, Intent intent,
+            IServiceConnection connection, @Context.BindServiceFlags int flags) {
         if (mService == null) {
-            return;
+            return false;
         }
         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 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.
-     * @hide
-     */
-    public void unbindRemoteViewsService(String packageName, int appWidgetId, Intent intent) {
-        if (mService == null) {
-            return;
-        }
-        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 51277e4..e5ae0ca 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.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.ViewGroup;
 import android.widget.RemoteViews.OnClickHandler;
 
-import com.android.internal.widget.IRemoteViewsAdapterConnection;
 import com.android.internal.widget.IRemoteViewsFactory;
 
 import java.lang.ref.WeakReference;
@@ -49,52 +50,33 @@
 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;
-
-    // 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;
-
-    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;
-    // items may be interrupted within the normally processed queues
-    private Handler mWorkerQueue;
-    private Handler mMainQueue;
+    private static final int DEFAULT_LOADING_VIEW_HEIGHT = 50;
 
     // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
     // structures;
@@ -111,6 +93,26 @@
     // 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 OnClickHandler mRemoteViewsOnClickHandler;
+    private final FixedSizeRemoteViewsCache mCache;
+    private int mVisibleWindowLowerBound;
+    private int mVisibleWindowUpperBound;
+
+    // The set of requested views that are to be notified when the associated RemoteViews are
+    // loaded.
+    private RemoteViewsFrameLayoutRefSet mRequestedViews;
+
+    private final HandlerThread mWorkerThread;
+    // items may be interrupted within the normally processed queues
+    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).
     private boolean mDataReady = false;
@@ -158,154 +160,192 @@
         }
     }
 
+    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;
-                }
-            }
-        }
-
-        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;
-            }
-        }
-
-        public synchronized void onServiceConnected(IBinder service) {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            // This is called on the same thread.
             mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
+            enqueueDeferredUnbindServiceMessage();
 
-            // Remove any deferred unbind messages
-            final RemoteViewsAdapter adapter = mAdapter.get();
-            if (adapter == null) return;
+            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());
-                        }
-
-                        // 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();
-                                }
-                            }
-                        });
-                    }
-
-                    // Enqueue unbind message
-                    adapter.enqueueDeferredUnbindServiceMessage();
-                    mIsConnected = true;
-                    mIsConnecting = false;
+            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 onServiceDisconnected() {
-            mIsConnected = false;
-            mIsConnecting = 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);
+            }
+        }
 
-            // Clear the main/worker queues
-            final RemoteViewsAdapter adapter = mAdapter.get();
-            if (adapter == null) return;
+        @Override
+        public void handleMessage(Message msg) {
+            RemoteViewsAdapter adapter = mAdapter.get();
 
-            adapter.mMainQueue.post(new Runnable() {
-                @Override
-                public void run() {
-                    // Dequeue any unbind messages
-                    adapter.mMainQueue.removeMessages(sUnbindServiceMessageType);
-
-                    final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
-                    if (callback != null) {
-                        callback.onRemoteAdapterDisconnected();
+            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;
+                    }
+
+                    // Flush the cache so that we can reload new items from the service
+                    synchronized (adapter.mCache) {
+                        adapter.mCache.reset();
+                    }
+
+                    // 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);
+                    }
+
+                    // 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);
+                        }
+                    }
+
+                    // 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;
+                }
+
+                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);
+
+                        // 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 @@
      *
      */
     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 @@
                 }
             }
 
-            int count = 0;
+            int count;
             synchronized (mMetaData) {
                 count = mMetaData.count;
             }
@@ -786,9 +825,11 @@
         // 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 @@
             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 @@
                     }
                 }
             } else {
-                mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
+                mCache = new FixedSizeRemoteViewsCache(DEFAULT_CACHE_SIZE);
             }
             if (!mDataReady) {
                 requestBindService();
@@ -825,9 +862,8 @@
     @Override
     protected void finalize() throws Throwable {
         try {
-            if (mWorkerThread != null) {
-                mWorkerThread.quit();
-            }
+            mServiceHandler.unbindNow();
+            mWorkerThread.quit();
         } finally {
             super.finalize();
         }
@@ -864,16 +900,13 @@
                 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 @@
         }
     }
 
-    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 @@
                 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 @@
         }
         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 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 @@
         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 @@
             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 @@
                         false);
                 mRequestedViews.add(position, layout);
                 mCache.queueRequestedPositionToLoad(position);
-                loadNextIndexInBackground();
+                mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM);
             }
             return layout;
         }
@@ -1158,69 +1140,12 @@
         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 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 @@
 
     @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;
             }
-            result = true;
-            break;
-        default:
-            break;
+            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;
+            }
         }
-        return result;
+        return false;
     }
 
-    private void enqueueDeferredUnbindServiceMessage() {
-        // Remove any existing deferred-unbind messages
-        mMainQueue.removeMessages(sUnbindServiceMessageType);
-        mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
-    }
-
-    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 @@
             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 caf35b3..a4da6b9c 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -26,6 +26,8 @@
 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 @@
     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 7294124..0000000
--- 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 0000000..06b860a
--- /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 51afada..76e7782 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_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.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 @@
         }
     };
 
-    // 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 @@
     }
 
     @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 @@
             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);
+            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) {
 
-            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);
+                    // 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 @@
 
     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 @@
         }
     }
 
-    // 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 @@
     // 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 @@
         }
     }
 
-    /**
-     * 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 @@
                         // 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);
                     }
                 }