1301 lines
52 KiB
Java
1301 lines
52 KiB
Java
/*
|
|
* 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<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 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<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.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<ArrayList<RemoteViewsFrameLayout>> {
|
|
|
|
/**
|
|
* Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
|
|
*/
|
|
public void add(int position, RemoteViewsFrameLayout layout) {
|
|
ArrayList<RemoteViewsFrameLayout> 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<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, 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<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 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<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(@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);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|