/* * Copyright (C) 2007 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 android.widget.RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID; import static android.widget.RemoteViews.EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND; import android.annotation.Nullable; import android.annotation.WorkerThread; import android.app.IServiceConnection; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.RemoteViews.InteractionHandler; import com.android.internal.widget.IRemoteViewsFactory; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.concurrent.Executor; /** * 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 */ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback { private static final String TAG = "RemoteViewsAdapter"; // The max number of items in the cache 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 UNBIND_SERVICE_DELAY = 5000; // Default height for the default loading view, in case we cannot get inflate the first view private static final int DEFAULT_LOADING_VIEW_HEIGHT = 50; // We cache the FixedSizeRemoteViewsCaches across orientation and re-inflation due to color // palette changes. These are the related data structures: private static final HashMap sCachedRemoteViewsCaches = new HashMap<>(); private static final HashMap 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 Context mContext; private final Intent mIntent; private final int mAppWidgetId; private final boolean mOnLightBackground; private final Executor mAsyncViewLoadExecutor; private InteractionHandler mRemoteViewsInteractionHandler; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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; /** * USed to dedupe {@link RemoteViews#mApplication} so that we do not hold on to * multiple copies of the same ApplicationInfo object. */ private ApplicationInfo mLastRemoteViewAppInfo; /** * An interface for the RemoteAdapter to notify other classes when adapters * are actually connected to/disconnected from their actual services. */ public interface RemoteAdapterConnectionCallback { /** * @return whether the adapter was set or not. */ boolean onRemoteAdapterConnected(); void onRemoteAdapterDisconnected(); /** * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not * connected yet. */ void deferNotifyDataSetChanged(); void setRemoteViewsAdapter(Intent intent, boolean isAsync); } public static class AsyncRemoteAdapterAction implements Runnable { private final RemoteAdapterConnectionCallback mCallback; private final Intent mIntent; public AsyncRemoteAdapterAction(RemoteAdapterConnectionCallback callback, Intent intent) { mCallback = callback; mIntent = intent; } @Override public void run() { mCallback.setRemoteViewsAdapter(mIntent, true); } } 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; /** * Handler for various interactions with the {@link RemoteViewsService}. */ private static class RemoteServiceHandler extends Handler implements ServiceConnection { private final WeakReference mAdapter; private final Context mContext; private IRemoteViewsFactory mRemoteViewsFactory; // 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; } @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); } } @Override public void onServiceDisconnected(ComponentName name) { mRemoteViewsFactory = null; RemoteViewsAdapter adapter = mAdapter.get(); if (adapter != null) { adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED); } } @Override public void handleMessage(Message msg) { RemoteViewsAdapter adapter = mAdapter.get(); 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; try { mBindRequested = AppWidgetManager.getInstance(mContext) .bindRemoteViewsService(mContext, appWidgetId, intent, sd, flags); } catch (Exception e) { Log.e(TAG, "Failed to bind remoteViewsService: " + e.getMessage()); } 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; } } } protected void unbindNow() { if (mBindRequested) { mBindRequested = false; mContext.unbindService(this); } mRemoteViewsFactory = null; } 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); } } /** * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when * they are loaded. */ static class RemoteViewsFrameLayout extends AppWidgetHostView.AdapterChildHostView { private final FixedSizeRemoteViewsCache mCache; public int cacheIndex = -1; public RemoteViewsFrameLayout(Context context, FixedSizeRemoteViewsCache cache) { super(context); mCache = cache; } /** * Updates this RemoteViewsFrameLayout depending on the view that was loaded. * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded * successfully. * @param forceApplyAsync when true, the host will always try to inflate the view * asynchronously (for eg, when we are already showing the loading * view) */ public void onRemoteViewsLoaded(RemoteViews view, InteractionHandler handler, boolean forceApplyAsync) { setInteractionHandler(handler); applyRemoteViews(view, forceApplyAsync || ((view != null) && view.prefersAsyncApply())); } /** * Creates a default loading view. Uses the size of the first row as a guide for the * size of the loading view. */ @Override protected View getDefaultView() { int viewHeight = mCache.getMetaData().getLoadingTemplate(getContext()).defaultHeight; // Compose the loading view text TextView loadingTextView = (TextView) LayoutInflater.from(getContext()).inflate( com.android.internal.R.layout.remote_views_adapter_default_loading_view, this, false); loadingTextView.setHeight(viewHeight); return loadingTextView; } @Override protected View getErrorView() { // Use the default loading view as the error view. return getDefaultView(); } } /** * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the * adapter that have not yet had their RemoteViews loaded. */ private class RemoteViewsFrameLayoutRefSet extends SparseArray> { /** * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter. */ public void add(int position, RemoteViewsFrameLayout layout) { ArrayList refs = get(position); // Create the list if necessary if (refs == null) { refs = new ArrayList<>(); put(position, refs); } // Add the references to the list layout.cacheIndex = position; refs.add(layout); } /** * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that * the associated RemoteViews has loaded. */ public void notifyOnRemoteViewsLoaded(int position, RemoteViews view) { if (view == null) return; // Remove this set from the original mapping final ArrayList refs = removeReturnOld(position); if (refs != null) { // Notify all the references for that position of the newly loaded RemoteViews for (final RemoteViewsFrameLayout ref : refs) { ref.onRemoteViewsLoaded(view, mRemoteViewsInteractionHandler, true); } } } /** * We need to remove views from this set if they have been recycled by the AdapterView. */ public void removeView(RemoteViewsFrameLayout rvfl) { if (rvfl.cacheIndex < 0) { return; } final ArrayList refs = get(rvfl.cacheIndex); if (refs != null) { refs.remove(rvfl); } rvfl.cacheIndex = -1; } } /** * The meta-data associated with the cache in it's current state. */ private static class RemoteViewsMetaData { int count; int viewTypeCount; boolean hasStableIds; // Used to determine how to construct loading views. If a loading view is not specified // by the user, then we try and load the first view, and use its height as the height for // the default loading view. LoadingViewTemplate loadingTemplate; // A mapping from type id to a set of unique type ids private final SparseIntArray mTypeIdIndexMap = new SparseIntArray(); public RemoteViewsMetaData() { reset(); } public void set(RemoteViewsMetaData d) { synchronized (d) { count = d.count; viewTypeCount = d.viewTypeCount; hasStableIds = d.hasStableIds; loadingTemplate = d.loadingTemplate; } } public void reset() { count = 0; // by default there is at least one placeholder view type viewTypeCount = 1; hasStableIds = true; loadingTemplate = null; mTypeIdIndexMap.clear(); } public int getMappedViewType(int typeId) { int mappedTypeId = mTypeIdIndexMap.get(typeId, -1); if (mappedTypeId == -1) { // We +1 because the loading view always has view type id of 0 mappedTypeId = mTypeIdIndexMap.size() + 1; mTypeIdIndexMap.put(typeId, mappedTypeId); } return mappedTypeId; } public boolean isViewTypeInRange(int typeId) { int mappedType = getMappedViewType(typeId); return (mappedType < viewTypeCount); } public synchronized LoadingViewTemplate getLoadingTemplate(Context context) { if (loadingTemplate == null) { loadingTemplate = new LoadingViewTemplate(null, context); } return loadingTemplate; } } /** * The meta-data associated with a single item in the cache. */ private static class RemoteViewsIndexMetaData { int typeId; long itemId; public RemoteViewsIndexMetaData(RemoteViews v, long itemId) { set(v, itemId); } public void set(RemoteViews v, long id) { itemId = id; if (v != null) { typeId = v.getLayoutId(); } else { typeId = 0; } } } /** * Config diff flags for which the cache should be reset */ private static final int CACHE_RESET_CONFIG_FLAGS = ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_ASSETS_PATHS; /** * */ private static class 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 // of the FixedSizeRemoteViewsCache. If we ever lock on both meta data objects, it is in // the order mTemporaryMetaData followed by mMetaData. private final RemoteViewsMetaData mMetaData = new RemoteViewsMetaData(); private final RemoteViewsMetaData mTemporaryMetaData = new RemoteViewsMetaData(); // The cache/mapping of position to RemoteViewsMetaData. This set is guaranteed to be // greater than or equal to the set of RemoteViews. // Note: The reason that we keep this separate from the RemoteViews cache below is that this // we still need to be able to access the mapping of position to meta data, without keeping // the heavy RemoteViews around. The RemoteViews cache is trimmed to fixed constraints wrt. // memory and size, but this metadata cache will retain information until the data at the // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged). private final SparseArray mIndexMetaData = new SparseArray<>(); // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses // too much memory. private final SparseArray mIndexRemoteViews = new SparseArray<>(); // An array of indices to load, Indices which are explicitly requested are set to true, // and those determined by the preloading algorithm to prefetch are set to false. private final SparseBooleanArray mIndicesToLoad = new SparseBooleanArray(); // We keep a reference of the last requested index to determine which item to prune the // farthest items from when we hit the memory limit private int mLastRequestedIndex; // The lower and upper bounds of the preloaded range private int mPreloadLowerBound; private int mPreloadUpperBound; // The bounds of this fixed cache, we will try and fill as many items into the cache up to // the maxCount number of items, or the maxSize memory usage. // The maxCountSlack is used to determine if a new position in the cache to be loaded is // sufficiently ouside the old set, prompting a shifting of the "window" of items to be // preloaded. private final int mMaxCount; private final int mMaxCountSlack; private static final float sMaxCountSlackPercent = 0.75f; private static final int sMaxMemoryLimitInBytes = 2 * 1024 * 1024; // Configuration for which the cache was created private final Configuration mConfiguration; FixedSizeRemoteViewsCache(int maxCacheSize, Configuration configuration) { mMaxCount = maxCacheSize; mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2)); mPreloadLowerBound = 0; mPreloadUpperBound = -1; mLastRequestedIndex = -1; mConfiguration = new Configuration(configuration); } public void insert(int position, RemoteViews v, long itemId, int[] visibleWindow) { // Trim the cache if we go beyond the count if (mIndexRemoteViews.size() >= mMaxCount) { mIndexRemoteViews.remove(getFarthestPositionFrom(position, visibleWindow)); } // Trim the cache if we go beyond the available memory size constraints int pruneFromPosition = (mLastRequestedIndex > -1) ? mLastRequestedIndex : position; while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryLimitInBytes) { // Note: This is currently the most naive mechanism for deciding what to prune when // we hit the memory limit. In the future, we may want to calculate which index to // remove based on both its position as well as it's current memory usage, as well // as whether it was directly requested vs. whether it was preloaded by our caching // mechanism. int trimIndex = getFarthestPositionFrom(pruneFromPosition, visibleWindow); // Need to check that this is a valid index, to cover the case where you have only // a single view in the cache, but it's larger than the max memory limit if (trimIndex < 0) { break; } mIndexRemoteViews.remove(trimIndex); } // Update the metadata cache final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position); if (metaData != null) { metaData.set(v, itemId); } else { mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId)); } mIndexRemoteViews.put(position, v); } public RemoteViewsMetaData getMetaData() { return mMetaData; } public RemoteViewsMetaData getTemporaryMetaData() { return mTemporaryMetaData; } public RemoteViews getRemoteViewsAt(int position) { return mIndexRemoteViews.get(position); } public RemoteViewsIndexMetaData getMetaDataAt(int position) { return mIndexMetaData.get(position); } public void commitTemporaryMetaData() { synchronized (mTemporaryMetaData) { synchronized (mMetaData) { mMetaData.set(mTemporaryMetaData); } } } private int getRemoteViewsBitmapMemoryUsage() { // Calculate the memory usage of all the RemoteViews bitmaps being cached int mem = 0; for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) { final RemoteViews v = mIndexRemoteViews.valueAt(i); if (v != null) { mem += v.estimateMemoryUsage(); } } return mem; } private int getFarthestPositionFrom(int pos, int[] visibleWindow) { // Find the index farthest away and remove that int maxDist = 0; int maxDistIndex = -1; int maxDistNotVisible = 0; int maxDistIndexNotVisible = -1; for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) { int index = mIndexRemoteViews.keyAt(i); int dist = Math.abs(index-pos); if (dist > maxDistNotVisible && Arrays.binarySearch(visibleWindow, index) < 0) { // maxDistNotVisible/maxDistIndexNotVisible will store the index of the // farthest non-visible position maxDistIndexNotVisible = index; maxDistNotVisible = dist; } if (dist >= maxDist) { // maxDist/maxDistIndex will store the index of the farthest position // regardless of whether it is visible or not maxDistIndex = index; maxDist = dist; } } if (maxDistIndexNotVisible > -1) { return maxDistIndexNotVisible; } return maxDistIndex; } public void queueRequestedPositionToLoad(int position) { mLastRequestedIndex = position; synchronized (mIndicesToLoad) { mIndicesToLoad.put(position, true); } } public boolean queuePositionsToBePreloadedFromRequestedPosition(int position) { // Check if we need to preload any items if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) { int center = (mPreloadUpperBound + mPreloadLowerBound) / 2; if (Math.abs(position - center) < mMaxCountSlack) { return false; } } int count; synchronized (mMetaData) { count = mMetaData.count; } synchronized (mIndicesToLoad) { // Remove all indices which have not been previously requested. for (int i = mIndicesToLoad.size() - 1; i >= 0; i--) { if (!mIndicesToLoad.valueAt(i)) { mIndicesToLoad.removeAt(i); } } // Add all the preload indices int halfMaxCount = mMaxCount / 2; mPreloadLowerBound = position - halfMaxCount; mPreloadUpperBound = position + halfMaxCount; int effectiveLowerBound = Math.max(0, mPreloadLowerBound); int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1); for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) { if (mIndexRemoteViews.indexOfKey(i) < 0 && !mIndicesToLoad.get(i)) { // If the index has not been requested, and has not been loaded. mIndicesToLoad.put(i, false); } } } return true; } /** Returns the next index to load */ public int getNextIndexToLoad() { // We try and prioritize items that have been requested directly, instead // of items that are loaded as a result of the caching mechanism synchronized (mIndicesToLoad) { // Prioritize requested indices to be loaded first int index = mIndicesToLoad.indexOfValue(true); if (index < 0) { // Otherwise, preload other indices as necessary index = mIndicesToLoad.indexOfValue(false); } if (index < 0) { return -1; } else { int key = mIndicesToLoad.keyAt(index); mIndicesToLoad.removeAt(index); return key; } } } public boolean containsRemoteViewAt(int position) { return mIndexRemoteViews.indexOfKey(position) >= 0; } public boolean containsMetaDataAt(int position) { return mIndexMetaData.indexOfKey(position) >= 0; } public void reset() { // Note: We do not try and reset the meta data, since that information is still used by // collection views to validate it's own contents (and will be re-requested if the data // is invalidated through the notifyDataSetChanged() flow). mPreloadLowerBound = 0; mPreloadUpperBound = -1; mLastRequestedIndex = -1; mIndexRemoteViews.clear(); mIndexMetaData.clear(); synchronized (mIndicesToLoad) { mIndicesToLoad.clear(); } } } static class RemoteViewsCacheKey { final Intent.FilterComparison filter; final int widgetId; RemoteViewsCacheKey(Intent.FilterComparison filter, int widgetId) { this.filter = filter; this.widgetId = widgetId; } @Override public boolean equals(@Nullable Object o) { if (!(o instanceof RemoteViewsCacheKey)) { return false; } RemoteViewsCacheKey other = (RemoteViewsCacheKey) o; return other.filter.equals(filter) && other.widgetId == widgetId; } @Override public int hashCode() { return (filter == null ? 0 : filter.hashCode()) ^ (widgetId << 2); } } public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) { mContext = context; mIntent = intent; if (mIntent == null) { throw new IllegalArgumentException("Non-null Intent must be specified."); } mAppWidgetId = intent.getIntExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1); mRequestedViews = new RemoteViewsFrameLayoutRefSet(); mOnLightBackground = intent.getBooleanExtra(EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND, false); // Strip the previously injected app widget id from service intent intent.removeExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID); intent.removeExtra(EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND); // Initialize the worker thread mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); mWorkerThread.start(); 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"); sCacheRemovalThread.start(); sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper()); } RemoteViewsCacheKey key = new RemoteViewsCacheKey(new Intent.FilterComparison(mIntent), mAppWidgetId); synchronized(sCachedRemoteViewsCaches) { FixedSizeRemoteViewsCache cache = sCachedRemoteViewsCaches.get(key); Configuration config = context.getResources().getConfiguration(); if (cache == null || (cache.mConfiguration.diff(config) & CACHE_RESET_CONFIG_FLAGS) != 0) { mCache = new FixedSizeRemoteViewsCache(DEFAULT_CACHE_SIZE, config); } else { mCache = sCachedRemoteViewsCaches.get(key); synchronized (mCache.mMetaData) { if (mCache.mMetaData.count > 0) { // As a precautionary measure, we verify that the meta data indicates a // non-zero count before declaring that data is ready. mDataReady = true; } } } if (!mDataReady) { requestBindService(); } } } @Override protected void finalize() throws Throwable { try { mServiceHandler.unbindNow(); mWorkerThread.quit(); } finally { super.finalize(); } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public boolean isDataReady() { return mDataReady; } /** @hide */ public void setRemoteViewsInteractionHandler(InteractionHandler handler) { mRemoteViewsInteractionHandler = handler; } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void saveRemoteViewsCache() { final RemoteViewsCacheKey key = new RemoteViewsCacheKey( new Intent.FilterComparison(mIntent), mAppWidgetId); synchronized(sCachedRemoteViewsCaches) { // If we already have a remove runnable posted for this key, remove it. if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) { sCacheRemovalQueue.removeCallbacks(sRemoteViewsCacheRemoveRunnables.get(key)); sRemoteViewsCacheRemoveRunnables.remove(key); } int metaDataCount = 0; int numRemoteViewsCached = 0; synchronized (mCache.mMetaData) { metaDataCount = mCache.mMetaData.count; } synchronized (mCache) { numRemoteViewsCached = mCache.mIndexRemoteViews.size(); } if (metaDataCount > 0 && numRemoteViewsCached > 0) { sCachedRemoteViewsCaches.put(key, mCache); } Runnable r = () -> { synchronized (sCachedRemoteViewsCaches) { sCachedRemoteViewsCaches.remove(key); sRemoteViewsCacheRemoveRunnables.remove(key); } }; sRemoteViewsCacheRemoveRunnables.put(key, r); sCacheRemovalQueue.postDelayed(r, REMOTE_VIEWS_CACHE_DURATION); } } @WorkerThread private void updateTemporaryMetaData(IRemoteViewsFactory factory) { try { // get the properties/first view (so that we can use it to // measure our placeholder views) boolean hasStableIds = factory.hasStableIds(); int viewTypeCount = factory.getViewTypeCount(); int count = factory.getCount(); LoadingViewTemplate loadingTemplate = new LoadingViewTemplate(factory.getLoadingView(), mContext); if ((count > 0) && (loadingTemplate.remoteViews == null)) { RemoteViews firstView = factory.getViewAt(0); if (firstView != null) { loadingTemplate.loadFirstViewHeight(firstView, mContext, new HandlerThreadExecutor(mWorkerThread)); } } final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData(); synchronized (tmpMetaData) { tmpMetaData.hasStableIds = hasStableIds; // We +1 because the base view type is the loading view tmpMetaData.viewTypeCount = viewTypeCount + 1; tmpMetaData.count = count; tmpMetaData.loadingTemplate = loadingTemplate; } } 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); } } @WorkerThread private void updateRemoteViews(IRemoteViewsFactory factory, int position, boolean notifyWhenLoaded) { // Load the item information from the remote service final RemoteViews remoteViews; final long itemId; try { remoteViews = factory.getViewAt(position); itemId = factory.getItemId(position); 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; } if (remoteViews.mApplication != null) { // We keep track of last application info. This helps when all the remoteViews have // same applicationInfo, which should be the case for a typical adapter. But if every // view has different application info, there will not be any optimization. if (mLastRemoteViewAppInfo != null && remoteViews.hasSameAppInfo(mLastRemoteViewAppInfo)) { // We should probably also update the remoteViews for nested ViewActions. // Hopefully, RemoteViews in an adapter would be less complicated. remoteViews.mApplication = mLastRemoteViewAppInfo; } else { mLastRemoteViewAppInfo = remoteViews.mApplication; } } int layoutId = remoteViews.getLayoutId(); RemoteViewsMetaData metaData = mCache.getMetaData(); boolean viewTypeInRange; int cacheCount; synchronized (metaData) { viewTypeInRange = metaData.isViewTypeInRange(layoutId); cacheCount = mCache.mMetaData.count; } synchronized (mCache) { if (viewTypeInRange) { int[] visibleWindow = getVisibleWindow(cacheCount); // Cache the RemoteViews we loaded mCache.insert(position, remoteViews, itemId, visibleWindow); if (notifyWhenLoaded) { // 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 // factory is less than the number of view types returned. We don't return this // view to the AdapterView, as this will cause an exception in the hosting process, // which contains the associated AdapterView. Log.e(TAG, "Error: widget's RemoteViewsFactory returns more view types than " + " indicated by getViewTypeCount() "); } } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public Intent getRemoteViewsServiceIntent() { return mIntent; } public int getCount() { final RemoteViewsMetaData metaData = mCache.getMetaData(); synchronized (metaData) { return metaData.count; } } public Object getItem(int position) { // Disallow arbitrary object to be associated with an item for the time being return null; } public long getItemId(int position) { synchronized (mCache) { if (mCache.containsMetaDataAt(position)) { return mCache.getMetaDataAt(position).itemId; } return 0; } } public int getItemViewType(int position) { final int typeId; synchronized (mCache) { if (mCache.containsMetaDataAt(position)) { typeId = mCache.getMetaDataAt(position).typeId; } else { return 0; } } final RemoteViewsMetaData metaData = mCache.getMetaData(); synchronized (metaData) { return metaData.getMappedViewType(typeId); } } /** * This method allows an AdapterView using this Adapter to provide information about which * views are currently being displayed. This allows for certain optimizations and preloading * which wouldn't otherwise be possible. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void setVisibleRangeHint(int lowerBound, int upperBound) { mVisibleWindowLowerBound = lowerBound; mVisibleWindowUpperBound = upperBound; } public View getView(int position, View convertView, ViewGroup parent) { // "Request" an index so that we can queue it for loading, initiate subsequent // preloading, etc. synchronized (mCache) { RemoteViews rv = mCache.getRemoteViewsAt(position); boolean isInCache = (rv != null); boolean hasNewItems = false; if (convertView != null && convertView instanceof RemoteViewsFrameLayout) { mRequestedViews.removeView((RemoteViewsFrameLayout) convertView); } if (!isInCache) { // Requesting bind service will trigger a super.notifyDataSetChanged(), which will // in turn trigger another request to getView() requestBindService(); } else { // Queue up other indices to be preloaded based on this position hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position); } final RemoteViewsFrameLayout layout; if (convertView instanceof RemoteViewsFrameLayout) { layout = (RemoteViewsFrameLayout) convertView; } else { layout = new RemoteViewsFrameLayout(parent.getContext(), mCache); layout.setExecutor(mAsyncViewLoadExecutor); layout.setOnLightBackground(mOnLightBackground); } if (isInCache) { // Apply the view synchronously if possible, to avoid flickering layout.onRemoteViewsLoaded(rv, mRemoteViewsInteractionHandler, false); 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. layout.onRemoteViewsLoaded( mCache.getMetaData().getLoadingTemplate(mContext).remoteViews, mRemoteViewsInteractionHandler, false); mRequestedViews.add(position, layout); mCache.queueRequestedPositionToLoad(position); mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM); } return layout; } } public int getViewTypeCount() { final RemoteViewsMetaData metaData = mCache.getMetaData(); synchronized (metaData) { return metaData.viewTypeCount; } } public boolean hasStableIds() { final RemoteViewsMetaData metaData = mCache.getMetaData(); synchronized (metaData) { return metaData.hasStableIds; } } public boolean isEmpty() { return getCount() <= 0; } /** * Returns a sorted array of all integers between lower and upper. */ 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]; } int[] window; if (lower <= upper) { window = new int[upper + 1 - lower]; for (int i = lower, j = 0; i <= upper; i++, j++){ window[j] = i; } } else { // If the upper bound is less than the lower bound it means that the visible window // wraps around. count = Math.max(count, lower); window = new int[count - lower + upper + 1]; int j = 0; // Add the entries in sorted order for (int i = 0; i <= upper; i++, j++) { window[j] = i; } for (int i = lower; i < count; i++, j++) { window[j] = i; } } return window; } public void notifyDataSetChanged() { mServiceHandler.removeMessages(MSG_UNBIND_SERVICE); mServiceHandler.sendEmptyMessage(MSG_NOTIFY_DATA_SET_CHANGED); } void superNotifyDataSetChanged() { super.notifyDataSetChanged(); } @Override public boolean handleMessage(Message msg) { switch (msg.what) { 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; } } return false; } private void requestBindService() { mServiceHandler.removeMessages(MSG_UNBIND_SERVICE); Message.obtain(mServiceHandler, MSG_REQUEST_BIND, mAppWidgetId, 0, mIntent).sendToTarget(); } private static class HandlerThreadExecutor implements Executor { private final HandlerThread mThread; HandlerThreadExecutor(HandlerThread thread) { mThread = thread; } @Override public void execute(Runnable runnable) { if (Thread.currentThread().getId() == mThread.getId()) { runnable.run(); } else { new Handler(mThread.getLooper()).post(runnable); } } } private static class LoadingViewTemplate { public final RemoteViews remoteViews; public int defaultHeight; LoadingViewTemplate(RemoteViews views, Context context) { remoteViews = views; float density = context.getResources().getDisplayMetrics().density; defaultHeight = Math.round(DEFAULT_LOADING_VIEW_HEIGHT * density); } public void loadFirstViewHeight( RemoteViews firstView, Context context, Executor executor) { // Inflate the first view on the worker thread firstView.applyAsync(context, new RemoteViewsFrameLayout(context, null), executor, new RemoteViews.OnViewAppliedListener() { @Override public void onViewApplied(View v) { try { v.measure( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); defaultHeight = v.getMeasuredHeight(); } catch (Exception e) { onError(e); } } @Override public void onError(Exception e) { // Do nothing. The default height will stay the same. Log.w(TAG, "Error inflating first RemoteViews", e); } }); } } }