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 | |
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')
-rw-r--r-- | core/java/android/appwidget/AppWidgetHost.java | 23 | ||||
-rw-r--r-- | core/java/android/appwidget/AppWidgetHostView.java | 19 | ||||
-rw-r--r-- | core/java/android/appwidget/AppWidgetManager.java | 29 | ||||
-rw-r--r-- | core/java/android/widget/AbsListView.java | 73 | ||||
-rw-r--r-- | core/java/android/widget/GridView.java | 35 | ||||
-rw-r--r-- | core/java/android/widget/ListView.java | 32 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViews.java | 50 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViewsAdapter.java | 666 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViewsService.java | 140 |
9 files changed, 1065 insertions, 2 deletions
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index b2fc13f..6011eec 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -39,6 +39,7 @@ public class AppWidgetHost { static final int HANDLE_UPDATE = 1; static final int HANDLE_PROVIDER_CHANGED = 2; + static final int HANDLE_VIEW_DATA_CHANGED = 3; final static Object sServiceLock = new Object(); static IAppWidgetService sService; @@ -60,6 +61,14 @@ public class AppWidgetHost { msg.obj = info; msg.sendToTarget(); } + + public void viewDataChanged(int appWidgetId, RemoteViews views, int viewId) { + Message msg = mHandler.obtainMessage(HANDLE_VIEW_DATA_CHANGED); + msg.arg1 = appWidgetId; + msg.arg2 = viewId; + msg.obj = views; + msg.sendToTarget(); + } } class UpdateHandler extends Handler { @@ -77,6 +86,10 @@ public class AppWidgetHost { onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj); break; } + case HANDLE_VIEW_DATA_CHANGED: { + viewDataChanged(msg.arg1, (RemoteViews) msg.obj, msg.arg2); + break; + } } } } @@ -250,6 +263,16 @@ public class AppWidgetHost { v.updateAppWidget(views); } } + + void viewDataChanged(int appWidgetId, RemoteViews views, int viewId) { + AppWidgetHostView v; + synchronized (mViews) { + v = mViews.get(appWidgetId); + } + if (v != null) { + v.viewDataChanged(views, viewId); + } + } } diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 3c19ea3..22f4266 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -32,6 +32,9 @@ import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; @@ -258,6 +261,22 @@ public class AppWidgetHostView extends FrameLayout { } /** + * Process data-changed notifications for the specified view in the specified + * set of {@link RemoteViews} views. + */ + void viewDataChanged(RemoteViews remoteViews, int viewId) { + View v = findViewById(viewId); + if ((v != null) && (v instanceof AdapterView<?>)) { + AdapterView<?> adapterView = (AdapterView<?>) v; + Adapter adapter = adapterView.getAdapter(); + if (adapter instanceof BaseAdapter) { + BaseAdapter baseAdapter = (BaseAdapter) adapter; + baseAdapter.notifyDataSetChanged(); + } + } + } + + /** * Build a {@link Context} cloned into another package name, usually for the * purposes of reading remote resources. */ diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 3f12bf9..5ee721f 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -288,6 +288,35 @@ public class AppWidgetManager { } /** + * Notifies the specified collection view in all the specified AppWidget instances + * to invalidate their currently data. + * + * @param appWidgetIds The AppWidget instances for which to notify of view data changes. + * @param views The RemoteViews which contains the view referenced at viewId. + * @param viewId The collection view id. + */ + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, RemoteViews views, int viewId) { + try { + sService.notifyAppWidgetViewDataChanged(appWidgetIds, views, viewId); + } + catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + } + + /** + * Notifies the specified collection view in all the specified AppWidget instance + * to invalidate it's currently data. + * + * @param appWidgetId The AppWidget instance for which to notify of view data changes. + * @param views The RemoteViews which contains the view referenced at viewId. + * @param viewId The collection view id. + */ + public void notifyAppWidgetViewDataChanged(int appWidgetId, RemoteViews views, int viewId) { + notifyAppWidgetViewDataChanged(new int[] { appWidgetId }, views, viewId); + } + + /** * Return a list of the AppWidget providers that are currently installed. */ public List<AppWidgetProviderInfo> getInstalledProviders() { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 70c1e15..1658b2f 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -19,6 +19,7 @@ package android.widget; import com.android.internal.R; import android.content.Context; +import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -71,7 +72,8 @@ import java.util.List; */ public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, - ViewTreeObserver.OnTouchModeChangeListener { + ViewTreeObserver.OnTouchModeChangeListener, + RemoteViewsAdapter.RemoteAdapterConnectionCallback { /** * Disables the transcript mode. @@ -180,6 +182,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te ListAdapter mAdapter; /** + * The remote adapter containing the data to be displayed by this view to be set + */ + private RemoteViewsAdapter mRemoteAdapter; + + /** * Indicates whether the list selector should be drawn on top of the children or behind */ boolean mDrawSelectorOnTop = false; @@ -2893,6 +2900,42 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mFlingRunnable.startScroll(distance, duration); } + /** + * Allows RemoteViews to scroll relatively to a position. + */ + void smoothScrollByOffset(int position) { + int index = -1; + if (position < 0) { + index = getFirstVisiblePosition(); + } else if (position > 0) { + index = getLastVisiblePosition(); + } + + if (index > -1) { + View child = getChildAt(index - getFirstVisiblePosition()); + if (child != null) { + Rect visibleRect = new Rect(); + if (child.getGlobalVisibleRect(visibleRect)) { + // the child is partially visible + int childRectArea = child.getWidth() * child.getHeight(); + int visibleRectArea = visibleRect.width() * visibleRect.height(); + float visibleArea = (visibleRectArea / (float) childRectArea); + final float visibleThreshold = 0.75f; + if ((position < 0) && (visibleArea < visibleThreshold)) { + // the top index is not perceivably visible so offset + // to account for showing that top index as well + ++index; + } else if ((position > 0) && (visibleArea < visibleThreshold)) { + // the bottom index is not perceivably visible so offset + // to account for showing that bottom index as well + --index; + } + } + smoothScrollToPosition(Math.max(0, Math.min(getCount(), index + position))); + } + } + } + private void createScrollingCache() { if (mScrollingCacheEnabled && !mCachingStarted) { setChildrenDrawnWithCacheEnabled(true); @@ -3905,6 +3948,34 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService + * through the specified intent. + * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. + */ + public void setRemoteViewsAdapter(Intent intent) { + mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this); + } + + /** + * Called back when the adapter connects to the RemoteViewsService. + */ + public void onRemoteAdapterConnected() { + if (mRemoteAdapter != mAdapter) { + setAdapter(mRemoteAdapter); + } + } + + /** + * Called back when the adapter disconnects from the RemoteViewsService. + */ + public void onRemoteAdapterDisconnected() { + if (mRemoteAdapter == mAdapter) { + mRemoteAdapter = null; + setAdapter(null); + } + } + + /** * Sets the recycler listener to be notified whenever a View is set aside in * the recycler for later reuse. This listener can be used to free resources * associated to the View. diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index fe69a13..a5b3ed5 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -17,22 +17,25 @@ package android.widget; import android.content.Context; +import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; +import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.SoundEffectConstants; import android.view.animation.GridLayoutAnimationController; +import android.widget.RemoteViews.RemoteView; /** * A view that shows items in two-dimensional scrolling grid. The items in the * grid come from the {@link ListAdapter} associated with this view. */ +@RemoteView public class GridView extends AbsListView { public static final int NO_STRETCH = 0; public static final int STRETCH_SPACING = 1; @@ -107,6 +110,16 @@ public class GridView extends AbsListView { } /** + * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService + * through the specified intent. + * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. + */ + @android.view.RemotableViewMethod + public void setRemoteViewsAdapter(Intent intent) { + super.setRemoteViewsAdapter(intent); + } + + /** * Sets the data behind this GridView. * * @param adapter the adapter providing the grid's data @@ -740,6 +753,26 @@ public class GridView extends AbsListView { } /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + @android.view.RemotableViewMethod + public void smoothScrollToPosition(int position) { + super.smoothScrollToPosition(position); + } + + /** + * Smoothly scroll to the specified adapter position offset. The view will + * scroll such that the indicated position is displayed. + * @param offset The amount to offset from the adapter position to scroll to. + */ + @android.view.RemotableViewMethod + public void smoothScrollByOffset(int offset) { + super.smoothScrollByOffset(offset); + } + + /** * Fills the grid based on positioning the new selection relative to the old * selection. The new selection will be placed at, above, or below the * location of the new selection depending on how the selection is moving. diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 86913ae..ad9d930 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -20,6 +20,7 @@ import com.android.internal.R; import com.google.android.collect.Lists; import android.content.Context; +import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; @@ -41,6 +42,7 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; +import android.widget.RemoteViews.RemoteView; import java.util.ArrayList; @@ -65,6 +67,7 @@ import java.util.ArrayList; * @attr ref android.R.styleable#ListView_headerDividersEnabled * @attr ref android.R.styleable#ListView_footerDividersEnabled */ +@RemoteView public class ListView extends AbsListView { /** * Used to indicate a no preference for a position type. @@ -401,6 +404,16 @@ public class ListView extends AbsListView { } /** + * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService + * through the specified intent. + * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. + */ + @android.view.RemotableViewMethod + public void setRemoteViewsAdapter(Intent intent) { + super.setRemoteViewsAdapter(intent); + } + + /** * Sets the data behind this ListView. * * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, @@ -863,6 +876,25 @@ public class ListView extends AbsListView { return topSelectionPixel; } + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + @android.view.RemotableViewMethod + public void smoothScrollToPosition(int position) { + super.smoothScrollToPosition(position); + } + + /** + * Smoothly scroll to the specified adapter position offset. The view will + * scroll such that the indicated position is displayed. + * @param offset The amount to offset from the adapter position to scroll to. + */ + @android.view.RemotableViewMethod + public void smoothScrollByOffset(int offset) { + super.smoothScrollByOffset(offset); + } /** * Fills the list based on positioning the new selection relative to the old diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index fc02acf..bbc75b9 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -285,6 +285,7 @@ public class RemoteViews implements Parcelable, Filter { static final int URI = 11; static final int BITMAP = 12; static final int BUNDLE = 13; + static final int INTENT = 14; int viewId; String methodName; @@ -347,6 +348,9 @@ public class RemoteViews implements Parcelable, Filter { case BUNDLE: this.value = in.readBundle(); break; + case INTENT: + this.value = Intent.CREATOR.createFromParcel(in); + break; default: break; } @@ -402,6 +406,9 @@ public class RemoteViews implements Parcelable, Filter { case BUNDLE: out.writeBundle((Bundle) this.value); break; + case INTENT: + ((Intent)this.value).writeToParcel(out, flags); + break; default: break; } @@ -435,6 +442,8 @@ public class RemoteViews implements Parcelable, Filter { return Bitmap.class; case BUNDLE: return Bundle.class; + case INTENT: + return Intent.class; default: return null; } @@ -770,6 +779,37 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. + * + * @param viewId The id of the view whose text should change + * @param intent The intent of the service which will be + * providing data to the RemoteViewsAdapter + */ + public void setRemoteAdapter(int viewId, Intent intent) { + setIntent(viewId, "setRemoteViewsAdapter", intent); + } + + /** + * Equivalent to calling {@link android.widget.AbsListView#smoothScrollToPosition(int, int)}. + * + * @param viewId The id of the view whose text should change + * @param position Scroll to this adapter position + */ + public void setScrollPosition(int viewId, int position) { + setInt(viewId, "smoothScrollToPosition", position); + } + + /** + * Equivalent to calling {@link android.widget.AbsListView#smoothScrollToPosition(int, int)}. + * + * @param viewId The id of the view whose text should change + * @param position Scroll by this adapter position offset + */ + public void setRelativeScrollPosition(int viewId, int offset) { + setInt(viewId, "smoothScrollByOffset", offset); + } + + /** * Call a method taking one boolean on a view in the layout for this RemoteViews. * * @param viewId The id of the view whose text should change @@ -916,6 +956,16 @@ public class RemoteViews implements Parcelable, Filter { } /** + * + * @param viewId + * @param methodName + * @param value + */ + public void setIntent(int viewId, String methodName, Intent value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INTENT, value)); + } + + /** * Inflates the view hierarchy represented by this object and applies * all of the actions. * 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); + } + } +} diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java new file mode 100644 index 0000000..7c9d7ff --- /dev/null +++ b/core/java/android/widget/RemoteViewsService.java @@ -0,0 +1,140 @@ +/* + * 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.Map; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Pair; + +import com.android.internal.widget.IRemoteViewsFactory; + +/** + * The service to be connected to for a remote adapter to request RemoteViews. Users should + * extend the RemoteViewsService to provide the appropriate RemoteViewsFactory's used to + * populate the remote collection view (ListView, GridView, etc). + */ +public abstract class RemoteViewsService extends Service { + + private static final String LOG_TAG = "RemoteViewsService"; + + // multimap implementation for reference counting + private HashMap<Intent, Pair<RemoteViewsFactory, Integer>> mRemoteViewFactories; + private final Object mLock = new Object(); + + /** + * An interface for an adapter between a remote collection view (ListView, GridView, etc) and + * the underlying data for that view. The implementor is responsible for making a RemoteView + * for each item in the data set. + */ + public interface RemoteViewsFactory { + public void onCreate(); + public void onDestroy(); + + public int getCount(); + public RemoteViews getViewAt(int position); + public RemoteViews getLoadingView(); + public int getViewTypeCount(); + public long getItemId(int position); + public boolean hasStableIds(); + } + + /** + * A private proxy class for the private IRemoteViewsFactory interface through the + * public RemoteViewsFactory interface. + */ + private class RemoteViewsFactoryAdapter extends IRemoteViewsFactory.Stub { + public RemoteViewsFactoryAdapter(RemoteViewsFactory factory) { + mFactory = factory; + } + + public int getCount() { + return mFactory.getCount(); + } + public RemoteViews getViewAt(int position) { + return mFactory.getViewAt(position); + } + public RemoteViews getLoadingView() { + return mFactory.getLoadingView(); + } + public int getViewTypeCount() { + return mFactory.getViewTypeCount(); + } + public long getItemId(int position) { + return mFactory.getItemId(position); + } + public boolean hasStableIds() { + return mFactory.hasStableIds(); + } + + private RemoteViewsFactory mFactory; + } + + public RemoteViewsService() { + mRemoteViewFactories = new HashMap<Intent, Pair<RemoteViewsFactory, Integer>>(); + } + + @Override + public IBinder onBind(Intent intent) { + synchronized (mLock) { + // increment the reference count to the particular factory associated with this intent + Pair<RemoteViewsFactory, Integer> factoryRef = null; + RemoteViewsFactory factory = null; + if (!mRemoteViewFactories.containsKey(intent)) { + factory = onGetViewFactory(intent); + factoryRef = new Pair<RemoteViewsFactory, Integer>(factory, 1); + mRemoteViewFactories.put(intent, factoryRef); + factory.onCreate(); + } else { + Pair<RemoteViewsFactory, Integer> oldFactoryRef = mRemoteViewFactories.get(intent); + factory = oldFactoryRef.first; + int newRefCount = oldFactoryRef.second.intValue() + 1; + factoryRef = new Pair<RemoteViewsFactory, Integer>(oldFactoryRef.first, newRefCount); + mRemoteViewFactories.put(intent, factoryRef); + } + return new RemoteViewsFactoryAdapter(factory); + } + } + + @Override + public boolean onUnbind(Intent intent) { + synchronized (mLock) { + if (mRemoteViewFactories.containsKey(intent)) { + // this alleviates the user's responsibility of having to clear all factories + Pair<RemoteViewsFactory, Integer> oldFactoryRef = mRemoteViewFactories.get(intent); + int newRefCount = oldFactoryRef.second.intValue() - 1; + if (newRefCount <= 0) { + oldFactoryRef.first.onDestroy(); + mRemoteViewFactories.remove(intent); + } else { + Pair<RemoteViewsFactory, Integer> factoryRef = new Pair<RemoteViewsFactory, Integer>(oldFactoryRef.first, newRefCount); + mRemoteViewFactories.put(intent, factoryRef); + } + } + } + return super.onUnbind(intent); + } + + /** + * To be implemented by the derived service to generate appropriate factories for + * the data. + */ + public abstract RemoteViewsFactory onGetViewFactory(Intent intent); +} |