| /* |
| * 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.HashMap; |
| import java.util.LinkedList; |
| 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(); |
| } |
| }); |
| } |
| }); |
| |
| // start the background loader |
| mViewCache.startBackgroundLoader(); |
| } |
| |
| public void onServiceDisconnected(ComponentName name) { |
| mRemoteViewsFactory = null; |
| mConnected = false; |
| |
| // clear the main/worker queues |
| mMainQueue.removeMessages(0); |
| |
| // stop the background loader |
| mViewCache.stopBackgroundLoader(); |
| |
| if (mCallback != null) |
| mCallback.onRemoteAdapterDisconnected(); |
| } |
| |
| 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; |
| private int[] mTmpViewCacheLoadIndices; |
| private LinkedList<Integer> mViewCacheLoadIndices; |
| private boolean mBackgroundLoaderEnabled; |
| |
| // 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; |
| |
| /** |
| * 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; |
| boolean isDataDirty; |
| Map<Integer, Integer> mTypeIdIndexMap; |
| |
| RemoteViewsInfo() { |
| count = 0; |
| // by default there is at least one dummy view type |
| viewTypeCount = 1; |
| hasStableIds = true; |
| isDataDirty = false; |
| mTypeIdIndexMap = new HashMap<Integer, Integer>(); |
| } |
| } |
| |
| public RemoteViewsCache(int halfCacheSize) { |
| mHalfCacheSize = halfCacheSize; |
| mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize); |
| mViewCacheStartPosition = 0; |
| mViewCacheEndPosition = -1; |
| mBackgroundLoaderEnabled = false; |
| |
| // initialize the cache |
| int cacheSize = 2 * mHalfCacheSize + 1; |
| mViewCacheInfo = new RemoteViewsInfo(); |
| mViewCache = new RemoteViewsIndexInfo[cacheSize]; |
| for (int i = 0; i < mViewCache.length; ++i) { |
| mViewCache[i] = new RemoteViewsIndexInfo(); |
| } |
| mTmpViewCacheLoadIndices = new int[cacheSize]; |
| mViewCacheLoadIndices = new LinkedList<Integer>(); |
| } |
| |
| private final boolean contains(int 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) { |
| // take the modulo of the 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 onNotifyDataSetChanged() { |
| // we mark the data as dirty so that the next call to fetch views will result in |
| // an onDataSetDirty() call from the adapter |
| synchronized (mViewCacheInfo) { |
| mViewCacheInfo.isDataDirty = true; |
| } |
| } |
| |
| private void updateNotifyDataSetChanged() { |
| // actually calls through to the factory to notify it to update |
| if (mServiceConnection.isConnected()) { |
| IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); |
| try { |
| factory.onDataSetChanged(); |
| } catch (RemoteException e) { |
| e.printStackTrace(); |
| } |
| |
| } |
| |
| // re-request the new metadata (only after the notification to the factory) |
| requestMetaData(); |
| |
| // post a new runnable on the main thread to propagate the notification back |
| // to the base adapter |
| mMainQueue.post(new Runnable() { |
| @Override |
| public void run() { |
| completeNotifyDataSetChanged(); |
| } |
| }); |
| } |
| |
| 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; |
| |
| // update the flipper |
| flipper.getChildAt(0).setVisibility(View.GONE); |
| boolean addNewView = true; |
| if (flipper.getChildCount() > 1) { |
| View v = flipper.getChildAt(1); |
| int typeId = ((Integer) v.getTag()).intValue(); |
| if (typeId == indexInfo.typeId) { |
| // we can reapply since it is the same type |
| indexInfo.view.reapply(mContext, v); |
| v.setVisibility(View.VISIBLE); |
| if (v.getAnimation() != null) |
| v.buildDrawingCache(); |
| addNewView = false; |
| } else { |
| flipper.removeViewAt(1); |
| } |
| } |
| if (addNewView) { |
| View v = indexInfo.view.apply(mContext, flipper); |
| v.setTag(new Integer(indexInfo.typeId)); |
| flipper.addView(v); |
| } |
| } |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) { |
| int indicesToLoadCount = 0; |
| |
| synchronized (mViewCache) { |
| 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; |
| int frameSize = mHalfCacheSize / 4; |
| int frameCount = (int) Math.ceil(mViewCache.length / (float) frameSize); |
| |
| // prune/add before the current start position |
| int effectiveStart = Math.max(newStartPosition, 0); |
| int effectiveEnd = Math.min(newEndPosition, getCount() - 1); |
| |
| // invalidate items in the queue |
| int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart); |
| int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd); |
| for (int i = 0; i < (frameSize * frameCount); ++i) { |
| int index = newStartPosition + ((i % frameSize) * frameCount + (i / frameSize)); |
| |
| if (index <= newEndPosition) { |
| if ((overlapStart <= index) && (index <= overlapEnd)) { |
| // load the stuff in the middle that has not already |
| // been loaded |
| if (!mViewCache[getCacheIndex(index)].isValid()) { |
| mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; |
| } |
| } else if ((effectiveStart <= index) && (index <= effectiveEnd)) { |
| // invalidate and load all new effective items |
| mViewCache[getCacheIndex(index)].invalidate(); |
| mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; |
| } else { |
| // invalidate all other cache indices (outside the effective start/end) |
| // but don't load |
| mViewCache[getCacheIndex(index)].invalidate(); |
| } |
| } |
| } |
| |
| mViewCacheStartPosition = newStartPosition; |
| mViewCacheEndPosition = newEndPosition; |
| } |
| } |
| |
| // post items to be loaded |
| int length = 0; |
| synchronized (mViewCacheInfo) { |
| length = mViewCacheInfo.count; |
| } |
| if (indicesToLoadCount > 0) { |
| synchronized (mViewCacheLoadIndices) { |
| mViewCacheLoadIndices.clear(); |
| for (int i = 0; i < indicesToLoadCount; ++i) { |
| final int index = mTmpViewCacheLoadIndices[i]; |
| if (0 <= index && index < length) { |
| mViewCacheLoadIndices.addLast(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) |
| initializeLoadingViews(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; |
| flipper.setVisibility(View.VISIBLE); |
| flipper.setAlpha(1.0f); |
| |
| 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); |
| } |
| } 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); |
| } |
| return flipper; |
| } |
| } |
| return new View(mContext); |
| } |
| |
| private void initializeLoadingViews(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 void startBackgroundLoader() { |
| // initialize the worker runnable |
| mBackgroundLoaderEnabled = true; |
| mWorkerQueue.post(new Runnable() { |
| @Override |
| public void run() { |
| while (mBackgroundLoaderEnabled) { |
| // notify the RemoteViews factory if necessary |
| boolean isDataDirty = false; |
| synchronized (mViewCacheInfo) { |
| isDataDirty = mViewCacheInfo.isDataDirty; |
| mViewCacheInfo.isDataDirty = false; |
| } |
| if (isDataDirty) { |
| updateNotifyDataSetChanged(); |
| } |
| |
| int index = -1; |
| synchronized (mViewCacheLoadIndices) { |
| if (!mViewCacheLoadIndices.isEmpty()) { |
| index = mViewCacheLoadIndices.removeFirst(); |
| } |
| } |
| if (index < 0) { |
| // there were no items to load, so sleep for a bit |
| try { |
| Thread.sleep(10); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| } else { |
| // otherwise, try and load the item |
| updateRemoteViewsInfo(index); |
| |
| // sleep for a bit to allow things to catch up after the load |
| try { |
| Thread.sleep(50); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| public void stopBackgroundLoader() { |
| // clear the items to be loaded |
| mBackgroundLoaderEnabled = false; |
| synchronized (mViewCacheLoadIndices) { |
| mViewCacheLoadIndices.clear(); |
| } |
| } |
| |
| 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() { |
| // clear the items to be loaded |
| synchronized (mViewCacheLoadIndices) { |
| mViewCacheLoadIndices.clear(); |
| } |
| |
| synchronized (mViewCache) { |
| // flush the internal cache and invalidate the adapter for future loads |
| 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(); |
| |
| // notify the factory that it's data may no longer be valid |
| mViewCache.onNotifyDataSetChanged(); |
| } |
| |
| public void completeNotifyDataSetChanged() { |
| 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); |
| } |
| } |
| } |