diff options
author | Winson Chung <winsonc@google.com> | 2010-07-16 11:18:17 -0700 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2010-07-19 14:48:31 -0700 |
commit | 499cb9f516062b654952d282f211bee44c31a3c2 (patch) | |
tree | 3c9bac8b31275e886bfbd07805c38839c185eab2 /core/java/android/widget/RemoteViewsAdapter.java | |
parent | b5b37f3bcc3065959c27e588f065dfb33a061e1d (diff) | |
download | frameworks_base-499cb9f516062b654952d282f211bee44c31a3c2.zip frameworks_base-499cb9f516062b654952d282f211bee44c31a3c2.tar.gz frameworks_base-499cb9f516062b654952d282f211bee44c31a3c2.tar.bz2 |
Initial changes to allow collections in widgets.
Change-Id: I3cfa899bae88cd252912cecebc12e93c27a3b7c9
Diffstat (limited to 'core/java/android/widget/RemoteViewsAdapter.java')
-rw-r--r-- | core/java/android/widget/RemoteViewsAdapter.java | 666 |
1 files changed, 666 insertions, 0 deletions
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java new file mode 100644 index 0000000..d426033 --- /dev/null +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -0,0 +1,666 @@ +/* + * 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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.MeasureSpec; + +import com.android.internal.widget.IRemoteViewsFactory; + +/** + * An adapter to a RemoteViewsService which fetches and caches RemoteViews + * to be later inflated as child views. + */ +/** @hide */ +public class RemoteViewsAdapter extends BaseAdapter { + + private static final String LOG_TAG = "RemoteViewsAdapter"; + + private Context mContext; + private Intent mIntent; + private RemoteViewsAdapterServiceConnection mServiceConnection; + private RemoteViewsCache mViewCache; + + private HandlerThread mWorkerThread; + // items may be interrupted within the normally processed queues + private Handler mWorkerQueue; + private Handler mMainQueue; + // items are never dequeued from the priority queue and must run + private Handler mWorkerPriorityQueue; + private Handler mMainPriorityQueue; + + /** + * An interface for the RemoteAdapter to notify other classes when adapters + * are actually connected to/disconnected from their actual services. + */ + public interface RemoteAdapterConnectionCallback { + public void onRemoteAdapterConnected(); + + public void onRemoteAdapterDisconnected(); + } + + /** + * The service connection that gets populated when the RemoteViewsService is + * bound. + */ + private class RemoteViewsAdapterServiceConnection implements ServiceConnection { + private boolean mConnected; + private IRemoteViewsFactory mRemoteViewsFactory; + private RemoteAdapterConnectionCallback mCallback; + + public RemoteViewsAdapterServiceConnection(RemoteAdapterConnectionCallback callback) { + mCallback = callback; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); + mConnected = true; + // notifyDataSetChanged should be called first, to ensure that the + // views are not updated twice + notifyDataSetChanged(); + + // post a new runnable to load the appropriate data, then callback + mWorkerPriorityQueue.post(new Runnable() { + @Override + public void run() { + // we need to get the viewTypeCount specifically, so just get all the + // metadata + mViewCache.requestMetaData(); + + // post a runnable to call the callback on the main thread + mMainPriorityQueue.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) + mCallback.onRemoteAdapterConnected(); + } + }); + } + }); + } + + public void onServiceDisconnected(ComponentName name) { + mRemoteViewsFactory = null; + mConnected = false; + if (mCallback != null) + mCallback.onRemoteAdapterDisconnected(); + + // clear the main/worker queues + mMainQueue.removeMessages(0); + mWorkerQueue.removeMessages(0); + } + + public IRemoteViewsFactory getRemoteViewsFactory() { + return mRemoteViewsFactory; + } + + public boolean isConnected() { + return mConnected; + } + } + + /** + * An internal cache of remote views. + */ + private class RemoteViewsCache { + private RemoteViewsInfo mViewCacheInfo; + private RemoteViewsIndexInfo[] mViewCache; + + // if a user loading view is not provided, then we create a temporary one + // for the user using the height of the first view + private RemoteViews mUserLoadingView; + private RemoteViews mFirstView; + private int mFirstViewHeight; + + // determines when the current cache window needs to be updated with new + // items (ie. when there is not enough slack) + private int mViewCacheStartPosition; + private int mViewCacheEndPosition; + private int mHalfCacheSize; + private int mCacheSlack; + private final float mCacheSlackPercentage = 0.75f; + + // determines whether to reorder the posted items on the worker thread + // so that the items in the current window can be loaded first + private int mPriorityLoadingWindowSize; + private int mPriorityLoadingWindowStart; + private int mPriorityLoadingWindowEnd; + + // determines which way to load items in the current window based on how + // the window shifted last + private boolean mLoadUpwards; + + /** + * The data structure stored at each index of the cache. Any member + * that is not invalidated persists throughout the lifetime of the cache. + */ + private class RemoteViewsIndexInfo { + FrameLayout flipper; + RemoteViews view; + long itemId; + int typeId; + + RemoteViewsIndexInfo() { + invalidate(); + } + + void set(RemoteViews v, long id) { + view = v; + itemId = id; + if (v != null) + typeId = v.getLayoutId(); + else + typeId = 0; + } + + void invalidate() { + view = null; + itemId = 0; + typeId = 0; + } + + final boolean isValid() { + return (view != null); + } + } + + /** + * Remote adapter metadata. Useful for when we have to lock on something + * before updating the metadata. + */ + private class RemoteViewsInfo { + int count; + int viewTypeCount; + boolean hasStableIds; + Map<Integer, Integer> mTypeIdIndexMap; + + RemoteViewsInfo() { + count = 0; + // by default there is at least one dummy view type + viewTypeCount = 1; + hasStableIds = true; + mTypeIdIndexMap = new HashMap<Integer, Integer>(); + } + } + + public RemoteViewsCache(int halfCacheSize) { + mHalfCacheSize = halfCacheSize; + mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize); + mViewCacheStartPosition = 0; + mViewCacheEndPosition = -1; + mPriorityLoadingWindowSize = 4; + mPriorityLoadingWindowStart = 0; + mPriorityLoadingWindowEnd = 0; + mLoadUpwards = false; + + // initialize the cache + mViewCacheInfo = new RemoteViewsInfo(); + mViewCache = new RemoteViewsIndexInfo[2 * mHalfCacheSize + 1]; + for (int i = 0; i < mViewCache.length; ++i) { + mViewCache[i] = new RemoteViewsIndexInfo(); + } + } + + private final boolean contains(int position) { + // take the modulo of the position + return (mViewCacheStartPosition <= position) && (position < mViewCacheEndPosition); + } + + private final boolean containsAndIsValid(int position) { + if (contains(position)) { + RemoteViewsIndexInfo indexInfo = mViewCache[getCacheIndex(position)]; + if (indexInfo.isValid()) { + return true; + } + } + return false; + } + + private final int getCacheIndex(int position) { + return (mViewCache.length + (position % mViewCache.length)) % mViewCache.length; + } + + public void requestMetaData() { + if (mServiceConnection.isConnected()) { + try { + IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); + + // 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(); + RemoteViews loadingView = factory.getLoadingView(); + RemoteViews firstView = null; + if ((count > 0) && (loadingView == null)) { + firstView = factory.getViewAt(0); + } + synchronized (mViewCacheInfo) { + RemoteViewsInfo info = mViewCacheInfo; + info.hasStableIds = hasStableIds; + info.viewTypeCount = viewTypeCount + 1; + info.count = count; + mUserLoadingView = loadingView; + if (firstView != null) { + mFirstView = firstView; + mFirstViewHeight = -1; + } + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } + + protected void updateRemoteViewsInfo(int position) { + if (mServiceConnection.isConnected()) { + IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); + + // load the item information + RemoteViews remoteView = null; + long itemId = 0; + try { + remoteView = factory.getViewAt(position); + itemId = factory.getItemId(position); + } catch (RemoteException e) { + e.printStackTrace(); + } + + synchronized (mViewCache) { + // skip if the window has moved + if (position < mViewCacheStartPosition || position >= mViewCacheEndPosition) + return; + + final int positionIndex = position; + final int cacheIndex = getCacheIndex(position); + mViewCache[cacheIndex].set(remoteView, itemId); + + // notify the main thread when done loading + // flush pending updates + mMainQueue.post(new Runnable() { + @Override + public void run() { + // swap the loader view for this view + synchronized (mViewCache) { + if (containsAndIsValid(positionIndex)) { + RemoteViewsIndexInfo indexInfo = mViewCache[cacheIndex]; + FrameLayout flipper = indexInfo.flipper; + + // recompose the flipper + View loadingView = flipper.getChildAt(0); + loadingView.setVisibility(View.GONE); + flipper.removeAllViews(); + flipper.addView(loadingView); + flipper.addView(indexInfo.view.apply(mContext, flipper)); + + // hide the loader view and bring the new view to the front + flipper.requestLayout(); + flipper.invalidate(); + } + } + } + }); + } + } + } + + private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) { + int indicesToLoadCount = 0; + int[] indicesToLoad = null; + + synchronized (mViewCache) { + indicesToLoad = new int[mViewCache.length]; + Arrays.fill(indicesToLoad, 0); + + if (containsAndIsValid(position)) { + // return the info if it exists in the window and is loaded + return mViewCache[getCacheIndex(position)]; + } + + // if necessary update the window and load the new information + int centerPosition = (mViewCacheEndPosition + mViewCacheStartPosition) / 2; + if ((mViewCacheEndPosition <= mViewCacheStartPosition) || (Math.abs(position - centerPosition) > mCacheSlack)) { + int newStartPosition = position - mHalfCacheSize; + int newEndPosition = position + mHalfCacheSize; + + // prune/add before the current start position + int effectiveStart = Math.max(newStartPosition, 0); + int effectiveEnd = Math.min(newEndPosition, getCount()); + + mWorkerQueue.removeMessages(0); + + // invalidate items in the queue + boolean loadFromBeginning = effectiveStart < mViewCacheStartPosition; + int numLoadFromBeginning = mViewCacheStartPosition - effectiveStart; + boolean loadFromEnd = effectiveEnd > mViewCacheEndPosition; + int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart); + int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd); + for (int i = newStartPosition; i < newEndPosition; ++i) { + if (loadFromBeginning && (effectiveStart <= i) && (i < overlapStart)) { + // load new items at the beginning in reverse order + mViewCache[getCacheIndex(i)].invalidate(); + indicesToLoad[indicesToLoadCount++] = effectiveStart + + (numLoadFromBeginning - (i - effectiveStart) - 1); + } else if (loadFromEnd && (overlapEnd <= i) && (i < effectiveEnd)) { + mViewCache[getCacheIndex(i)].invalidate(); + indicesToLoad[indicesToLoadCount++] = i; + } else if ((overlapStart <= i) && (i < overlapEnd)) { + // load the stuff in the middle that has not already + // been loaded + if (!mViewCache[getCacheIndex(i)].isValid()) { + indicesToLoad[indicesToLoadCount++] = i; + } + } else { + // invalidate all other cache indices (outside the effective start/end) + mViewCache[getCacheIndex(i)].invalidate(); + } + } + + mViewCacheStartPosition = newStartPosition; + mViewCacheEndPosition = newEndPosition; + mPriorityLoadingWindowStart = position; + mPriorityLoadingWindowEnd = position + mPriorityLoadingWindowSize; + mLoadUpwards = loadFromBeginning && !loadFromEnd; + } else if (contains(position)) { + // prioritize items around this position so that they load first + if (position < mPriorityLoadingWindowStart || position > mPriorityLoadingWindowEnd) { + mWorkerQueue.removeMessages(0); + + int index; + int effectiveStart = Math.max(position - mPriorityLoadingWindowSize, 0); + int effectiveEnd = 0; + synchronized (mViewCacheInfo) { + effectiveEnd = Math.min(position + mPriorityLoadingWindowSize - 1, + mViewCacheInfo.count - 1); + } + + for (int i = 0; i < mViewCache.length; ++i) { + if (mLoadUpwards) { + index = effectiveEnd - i; + } else { + index = effectiveStart + i; + } + if (!mViewCache[getCacheIndex(index)].isValid()) { + indicesToLoad[indicesToLoadCount++] = index; + } + } + + mPriorityLoadingWindowStart = effectiveStart; + mPriorityLoadingWindowEnd = position + mPriorityLoadingWindowSize; + } + } + } + + // post items to be loaded + int length = 0; + synchronized (mViewCacheInfo) { + length = mViewCacheInfo.count; + } + for (int i = 0; i < indicesToLoadCount; ++i) { + final int index = indicesToLoad[i]; + if (0 <= index && index < length) { + mWorkerQueue.post(new Runnable() { + @Override + public void run() { + updateRemoteViewsInfo(index); + } + }); + } + } + + // return null so that a dummy view can be retrieved + return null; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (mServiceConnection.isConnected()) { + // create the flipper views if necessary (we have to do this now + // for all the flippers while we have the reference to the parent) + createInitialLoadingFlipperViews(parent); + + // request the item from the cache (queueing it to load if not + // in the cache already) + RemoteViewsIndexInfo indexInfo = requestCachedIndexInfo(position); + + // update the flipper appropriately + synchronized (mViewCache) { + int cacheIndex = getCacheIndex(position); + FrameLayout flipper = mViewCache[cacheIndex].flipper; + + if (indexInfo == null) { + // hide the item view and show the loading view + flipper.getChildAt(0).setVisibility(View.VISIBLE); + for (int i = 1; i < flipper.getChildCount(); ++i) { + flipper.getChildAt(i).setVisibility(View.GONE); + } + flipper.requestLayout(); + flipper.invalidate(); + } else { + // hide the loading view and show the item view + for (int i = 0; i < flipper.getChildCount() - 1; ++i) { + flipper.getChildAt(i).setVisibility(View.GONE); + } + flipper.getChildAt(flipper.getChildCount() - 1).setVisibility(View.VISIBLE); + flipper.requestLayout(); + flipper.invalidate(); + } + return flipper; + } + } + return new View(mContext); + } + + private void createInitialLoadingFlipperViews(ViewGroup parent) { + // ensure that the cache has the appropriate initial flipper + synchronized (mViewCache) { + if (mViewCache[0].flipper == null) { + for (int i = 0; i < mViewCache.length; ++i) { + FrameLayout flipper = new FrameLayout(mContext); + if (mUserLoadingView != null) { + // use the user-specified loading view + flipper.addView(mUserLoadingView.apply(mContext, parent)); + } else { + // calculate the original size of the first row for the loader view + synchronized (mViewCacheInfo) { + if (mFirstViewHeight < 0) { + View firstView = mFirstView.apply(mContext, parent); + firstView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mFirstViewHeight = firstView.getMeasuredHeight(); + } + } + + // construct a new loader and add it to the flipper as the fallback + // default view + TextView textView = new TextView(mContext); + textView.setText("Loading..."); + textView.setHeight(mFirstViewHeight); + textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL); + textView.setTextSize(18.0f); + textView.setTextColor(Color.argb(96, 255, 255, 255)); + textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK); + + flipper.addView(textView); + } + mViewCache[i].flipper = flipper; + } + } + } + } + + public long getItemId(int position) { + synchronized (mViewCache) { + if (containsAndIsValid(position)) { + return mViewCache[getCacheIndex(position)].itemId; + } + } + return 0; + } + + public int getItemViewType(int position) { + // synchronize to ensure that the type id/index map is updated synchronously + synchronized (mViewCache) { + if (containsAndIsValid(position)) { + int viewId = mViewCache[getCacheIndex(position)].typeId; + Map<Integer, Integer> typeMap = mViewCacheInfo.mTypeIdIndexMap; + // we +1 because the default dummy view get view type 0 + if (typeMap.containsKey(viewId)) { + return typeMap.get(viewId); + } else { + int newIndex = typeMap.size() + 1; + typeMap.put(viewId, newIndex); + return newIndex; + } + } + } + // return the type of the default item + return 0; + } + + public int getCount() { + synchronized (mViewCacheInfo) { + return mViewCacheInfo.count; + } + } + + public int getViewTypeCount() { + synchronized (mViewCacheInfo) { + return mViewCacheInfo.viewTypeCount; + } + } + + public boolean hasStableIds() { + synchronized (mViewCacheInfo) { + return mViewCacheInfo.hasStableIds; + } + } + + public void flushCache() { + synchronized (mViewCache) { + // flush the internal cache and invalidate the adapter for future loads + mWorkerQueue.removeMessages(0); + mMainQueue.removeMessages(0); + + for (int i = 0; i < mViewCache.length; ++i) { + mViewCache[i].invalidate(); + } + + mViewCacheStartPosition = 0; + mViewCacheEndPosition = -1; + } + } + } + + public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) { + mContext = context; + mIntent = intent; + + // initialize the worker thread + mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); + mWorkerThread.start(); + mWorkerQueue = new Handler(mWorkerThread.getLooper()); + mWorkerPriorityQueue = new Handler(mWorkerThread.getLooper()); + mMainQueue = new Handler(Looper.myLooper()); + mMainPriorityQueue = new Handler(Looper.myLooper()); + + // initialize the cache and the service connection on startup + mViewCache = new RemoteViewsCache(25); + mServiceConnection = new RemoteViewsAdapterServiceConnection(callback); + requestBindService(); + } + + protected void finalize() throws Throwable { + // remember to unbind from the service when finalizing + unbindService(); + } + + public int getCount() { + requestBindService(); + return mViewCache.getCount(); + } + + 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) { + requestBindService(); + return mViewCache.getItemId(position); + } + + public int getItemViewType(int position) { + requestBindService(); + return mViewCache.getItemViewType(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + requestBindService(); + return mViewCache.getView(position, convertView, parent); + } + + public int getViewTypeCount() { + requestBindService(); + return mViewCache.getViewTypeCount(); + } + + public boolean hasStableIds() { + requestBindService(); + return mViewCache.hasStableIds(); + } + + public boolean isEmpty() { + return getCount() <= 0; + } + + public void notifyDataSetChanged() { + // flush the cache so that we can reload new items from the service + mViewCache.flushCache(); + super.notifyDataSetChanged(); + } + + private boolean requestBindService() { + // try binding the service (which will start it if it's not already running) + if (!mServiceConnection.isConnected()) { + mContext.bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + + return mServiceConnection.isConnected(); + } + + private void unbindService() { + if (mServiceConnection.isConnected()) { + mContext.unbindService(mServiceConnection); + } + } +} |