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.content.Context;
21import android.database.Cursor;
22import android.support.v4.app.Fragment;
23import android.support.v4.app.FragmentManager;
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    protected Context mContext;
41    protected Cursor mCursor;
42    protected int mRowIDColumn;
43    /** Mapping of row ID to cursor position */
44    protected SparseIntArray mItemPosition;
45    /** Mapping of instantiated object to row ID */
46    protected final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
47
48    /**
49     * Constructor that always enables auto-requery.
50     *
51     * @param c The cursor from which to get the data.
52     * @param context The context
53     */
54    public BaseCursorPagerAdapter(Context context, FragmentManager fm, Cursor c) {
55        super(fm);
56        init(context, c);
57    }
58
59    /**
60     * Makes a fragment for the data pointed to by the cursor
61     *
62     * @param context Interface to application's global information
63     * @param cursor The cursor from which to get the data. The cursor is already
64     * moved to the correct position.
65     * @return the newly created fragment.
66     */
67    public abstract Fragment getItem(Context context, Cursor cursor, int position);
68
69    // TODO: This shouldn't just return null - maybe it needs to wait for a cursor to be supplied?
70    //       See b/7103023
71    @Override
72    public Fragment getItem(int position) {
73        if (mCursor != null && moveCursorTo(position)) {
74            return getItem(mContext, mCursor, position);
75        }
76        return null;
77    }
78
79    @Override
80    public int getCount() {
81        if (mCursor != null) {
82            return mCursor.getCount();
83        } else {
84            return 0;
85        }
86    }
87
88    @Override
89    public Object instantiateItem(View container, int position) {
90        if (mCursor == null) {
91            throw new IllegalStateException("this should only be called when the cursor is valid");
92        }
93
94        final Integer rowId;
95        if (moveCursorTo(position)) {
96            rowId = mCursor.getString(mRowIDColumn).hashCode();
97        } else {
98            rowId = null;
99        }
100
101        // Create the fragment and bind cursor data
102        final Object obj = super.instantiateItem(container, position);
103        if (obj != null) {
104            mObjectRowMap.put(obj, rowId);
105        }
106        return obj;
107    }
108
109    @Override
110    public void destroyItem(View container, int position, Object object) {
111        mObjectRowMap.remove(object);
112
113        super.destroyItem(container, position, object);
114    }
115
116    @Override
117    public int getItemPosition(Object object) {
118        final Integer rowId = mObjectRowMap.get(object);
119        if (rowId == null || mItemPosition == null) {
120            return POSITION_NONE;
121        }
122
123        final int position = mItemPosition.get(rowId, POSITION_NONE);
124        return position;
125    }
126
127    /**
128     * @return true if data is valid
129     */
130    public boolean isDataValid() {
131        return mCursor != null;
132    }
133
134    /**
135     * Returns the cursor.
136     */
137    public Cursor getCursor() {
138        return mCursor;
139    }
140
141    /**
142     * Returns the data item associated with the specified position in the data set.
143     */
144    public Object getDataItem(int position) {
145        if (mCursor != null && moveCursorTo(position)) {
146            return mCursor;
147        } else {
148            return null;
149        }
150    }
151
152    /**
153     * Returns the row id associated with the specified position in the list.
154     */
155    public long getItemId(int position) {
156        if (mCursor != null && moveCursorTo(position)) {
157            return mCursor.getString(mRowIDColumn).hashCode();
158        } else {
159            return 0;
160        }
161    }
162
163    /**
164     * Swap in a new Cursor, returning the old Cursor.
165     *
166     * @param newCursor The new cursor to be used.
167     * @return Returns the previously set Cursor, or null if there was not one.
168     * If the given new Cursor is the same instance is the previously set
169     * Cursor, null is also returned.
170     */
171    public Cursor swapCursor(Cursor newCursor) {
172        if (Log.isLoggable(TAG, Log.VERBOSE)) {
173            Log.v(TAG, "swapCursor old=" + (mCursor == null ? -1 : mCursor.getCount()) +
174                    "; new=" + (newCursor == null ? -1 : newCursor.getCount()));
175        }
176
177        if (newCursor == mCursor) {
178            return null;
179        }
180        Cursor oldCursor = mCursor;
181        mCursor = newCursor;
182        if (newCursor != null) {
183            mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
184        } else {
185            mRowIDColumn = -1;
186        }
187
188        setItemPosition();
189        notifyDataSetChanged();     // notify the observers about the new cursor
190        return oldCursor;
191    }
192
193    /**
194     * Converts the cursor into a CharSequence. Subclasses should override this
195     * method to convert their results. The default implementation returns an
196     * empty String for null values or the default String representation of
197     * the value.
198     *
199     * @param cursor the cursor to convert to a CharSequence
200     * @return a CharSequence representing the value
201     */
202    public CharSequence convertToString(Cursor cursor) {
203        return cursor == null ? "" : cursor.toString();
204    }
205
206    @Override
207    protected String makeFragmentName(int viewId, int index) {
208        if (moveCursorTo(index)) {
209            return "android:pager:" + viewId + ":" + mCursor.getString(mRowIDColumn).hashCode();
210        } else {
211            return super.makeFragmentName(viewId, index);
212        }
213    }
214
215    /**
216     * Moves the cursor to the given position
217     *
218     * @return {@code true} if the cursor's position was set. Otherwise, {@code false}.
219     */
220    private boolean moveCursorTo(int position) {
221        if (mCursor != null && !mCursor.isClosed()) {
222            return mCursor.moveToPosition(position);
223        }
224        return false;
225    }
226
227    /**
228     * Initialize the adapter.
229     */
230    private void init(Context context, Cursor c) {
231        boolean cursorPresent = c != null;
232        mCursor = c;
233        mContext = context;
234        mRowIDColumn = cursorPresent
235                ? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
236    }
237
238    /**
239     * Sets the {@link #mItemPosition} instance variable with the current mapping of
240     * row id to cursor position.
241     */
242    private void setItemPosition() {
243        if (mCursor == null || mCursor.isClosed()) {
244            mItemPosition = null;
245            return;
246        }
247
248        SparseIntArray itemPosition = new SparseIntArray(mCursor.getCount());
249
250        mCursor.moveToPosition(-1);
251        while (mCursor.moveToNext()) {
252            final int rowId = mCursor.getString(mRowIDColumn).hashCode();
253            final int position = mCursor.getPosition();
254
255            itemPosition.append(rowId, position);
256        }
257        mItemPosition = itemPosition;
258    }
259}
260