diff options
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); } } |