content: /*
   * 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.UnsupportedAppUsage;
  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.ActivityInfo;
  import android.content.pm.ApplicationInfo;
  import android.content.res.Configuration;
  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.OnClickHandler;
  
  import com.android.internal.widget.IRemoteViewsFactory;
  
  import java.lang.ref.WeakReference;
  import java.util.Arrays;
  import java.util.HashMap;
  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.
   *
   * 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. 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 Context mContext;
      private final Intent mIntent;
      private final int mAppWidgetId;
      private final boolean mOnLightBackground;
      private final Executor mAsyncViewLoadExecutor;
  
      private OnClickHandler mRemoteViewsOnClickHandler;
      @UnsupportedAppUsage
      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
      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<RemoteViewsAdapter> 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 {
          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, OnClickHandler handler,
                  boolean forceApplyAsync) {
              setOnClickHandler(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 Context getRemoteContext() {
              return null;
          }
  
          @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<LinkedList<RemoteViewsFrameLayout>> {
  
          /**
           * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
           */
          public void add(int position, RemoteViewsFrameLayout layout) {
              LinkedList<RemoteViewsFrameLayout> refs = get(position);
  
              // Create the list if necessary
              if (refs == null) {
                  refs = new LinkedList<>();
                  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 LinkedList<RemoteViewsFrameLayout> 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, mRemoteViewsOnClickHandler, 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 LinkedList<RemoteViewsFrameLayout> 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 dummy 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<RemoteViewsIndexMetaData> 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<RemoteViews> 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(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
      public boolean isDataReady() {
          return mDataReady;
      }
  
      @UnsupportedAppUsage
      public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
          mRemoteViewsOnClickHandler = handler;
      }
  
      @UnsupportedAppUsage
      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) {
                      if (sCachedRemoteViewsCaches.containsKey(key)) {
                          sCachedRemoteViewsCaches.remove(key);
                      }
                      if (sRemoteViewsCacheRemoveRunnables.containsKey(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 dummy 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
      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
      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, mRemoteViewsOnClickHandler, 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,
                          mRemoteViewsOnClickHandler,
                          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);
                          }
                      });
          }
      }
  }
  