1/*
2 * Copyright (C) 2011 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.ex.photo.adapters;
19
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.content.Context;
23import android.database.Cursor;
24import android.util.Log;
25import android.util.SparseIntArray;
26import android.view.View;
27
28import com.android.ex.photo.provider.PhotoContract;
29
30import java.util.HashMap;
31
32/**
33 * Page adapter for use with an BaseCursorLoader. Unlike other cursor adapters, this has no
34 * observers for automatic refresh. Instead, it depends upon external mechanisms to provide
35 * the update signal.
36 */
37public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
38    private static final String TAG = "BaseCursorPagerAdapter";
39
40    Context mContext;
41    private boolean mDataValid;
42    private Cursor mCursor;
43    private int mRowIDColumn;
44    /** Mapping of row ID to cursor position */
45    private SparseIntArray mItemPosition;
46    /** Mapping of instantiated object to row ID */
47    private HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
48
49    /**
50     * Constructor that always enables auto-requery.
51     *
52     * @param c The cursor from which to get the data.
53     * @param context The context
54     */
55    public BaseCursorPagerAdapter(Context context, FragmentManager fm, Cursor c) {
56        super(fm);
57        init(context, c);
58    }
59
60    /**
61     * Makes a fragment for the data pointed to by the cursor
62     *
63     * @param context Interface to application's global information
64     * @param cursor The cursor from which to get the data. The cursor is already
65     * moved to the correct position.
66     * @return the newly created fragment.
67     */
68    public abstract Fragment getItem(Context context, Cursor cursor, int position);
69
70    @Override
71    public Fragment getItem(int position) {
72        if (mDataValid && moveCursorTo(position)) {
73            return getItem(mContext, mCursor, position);
74        }
75        return null;
76    }
77
78    @Override
79    public int getCount() {
80        if (mDataValid && mCursor != null) {
81            return mCursor.getCount();
82        } else {
83            return 0;
84        }
85    }
86
87    @Override
88    public Object instantiateItem(View container, int position) {
89        if (!mDataValid) {
90            throw new IllegalStateException("this should only be called when the cursor is valid");
91        }
92
93        final Integer rowId;
94        if (moveCursorTo(position)) {
95            rowId = mCursor.getString(mRowIDColumn).hashCode();
96        } else {
97            rowId = null;
98        }
99
100        // Create the fragment and bind cursor data
101        final Object obj = super.instantiateItem(container, position);
102        if (obj != null) {
103            mObjectRowMap.put(obj, rowId);
104        }
105        return obj;
106    }
107
108    @Override
109    public void destroyItem(View container, int position, Object object) {
110        mObjectRowMap.remove(object);
111
112        super.destroyItem(container, position, object);
113    }
114
115    @Override
116    public int getItemPosition(Object object) {
117        final Integer rowId = mObjectRowMap.get(object);
118        if (rowId == null || mItemPosition == null) {
119            return POSITION_NONE;
120        }
121
122        final int position = mItemPosition.get(rowId, POSITION_NONE);
123        return position;
124    }
125
126    /**
127     * @return true if data is valid
128     */
129    public boolean isDataValid() {
130        return mDataValid;
131    }
132
133    /**
134     * Returns the cursor.
135     */
136    public Cursor getCursor() {
137        return mCursor;
138    }
139
140    /**
141     * Returns the data item associated with the specified position in the data set.
142     */
143    public Object getDataItem(int position) {
144        if (mDataValid && moveCursorTo(position)) {
145            return mCursor;
146        } else {
147            return null;
148        }
149    }
150
151    /**
152     * Returns the row id associated with the specified position in the list.
153     */
154    public long getItemId(int position) {
155        if (mDataValid && moveCursorTo(position)) {
156            return mCursor.getString(mRowIDColumn).hashCode();
157        } else {
158            return 0;
159        }
160    }
161
162    /**
163     * Swap in a new Cursor, returning the old Cursor.
164     *
165     * @param newCursor The new cursor to be used.
166     * @return Returns the previously set Cursor, or null if there was not one.
167     * If the given new Cursor is the same instance is the previously set
168     * Cursor, null is also returned.
169     */
170    public Cursor swapCursor(Cursor newCursor) {
171        if (Log.isLoggable(TAG, Log.VERBOSE)) {
172            Log.v(TAG, "swapCursor old=" + (mCursor == null ? -1 : mCursor.getCount()) +
173                    "; new=" + (newCursor == null ? -1 : newCursor.getCount()));
174        }
175
176        if (newCursor == mCursor) {
177            return null;
178        }
179        Cursor oldCursor = mCursor;
180        mCursor = newCursor;
181        if (newCursor != null) {
182            mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
183            mDataValid = true;
184        } else {
185            mRowIDColumn = -1;
186            mDataValid = false;
187        }
188
189        setItemPosition();
190        notifyDataSetChanged();     // notify the observers about the new cursor
191        return oldCursor;
192    }
193
194    /**
195     * Converts the cursor into a CharSequence. Subclasses should override this
196     * method to convert their results. The default implementation returns an
197     * empty String for null values or the default String representation of
198     * the value.
199     *
200     * @param cursor the cursor to convert to a CharSequence
201     * @return a CharSequence representing the value
202     */
203    public CharSequence convertToString(Cursor cursor) {
204        return cursor == null ? "" : cursor.toString();
205    }
206
207    @Override
208    protected String makeFragmentName(int viewId, int index) {
209        if (moveCursorTo(index)) {
210            return "android:pager:" + viewId + ":" + mCursor.getString(mRowIDColumn).hashCode();
211        } else {
212            return super.makeFragmentName(viewId, index);
213        }
214    }
215
216    /**
217     * Moves the cursor to the given position
218     *
219     * @return {@code true} if the cursor's position was set. Otherwise, {@code false}.
220     */
221    private boolean moveCursorTo(int position) {
222        if (mCursor != null && !mCursor.isClosed()) {
223            return mCursor.moveToPosition(position);
224        }
225        return false;
226    }
227
228    /**
229     * Initialize the adapter.
230     */
231    private void init(Context context, Cursor c) {
232        boolean cursorPresent = c != null;
233        mCursor = c;
234        mDataValid = cursorPresent;
235        mContext = context;
236        mRowIDColumn = cursorPresent
237                ? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
238    }
239
240    /**
241     * Sets the {@link #mItemPosition} instance variable with the current mapping of
242     * row id to cursor position.
243     */
244    private void setItemPosition() {
245        if (!mDataValid || mCursor == null || mCursor.isClosed()) {
246            mItemPosition = null;
247            return;
248        }
249
250        SparseIntArray itemPosition = new SparseIntArray(mCursor.getCount());
251
252        mCursor.moveToPosition(-1);
253        while (mCursor.moveToNext()) {
254            final int rowId = mCursor.getString(mRowIDColumn).hashCode();
255            final int position = mCursor.getPosition();
256
257            itemPosition.append(rowId, position);
258        }
259        mItemPosition = itemPosition;
260    }
261}
262