ConversationPagerAdapter.java revision 3825f3d2284b2b57fadcfe6a4ebd9992f3c5c7bb
1/*
2 * Copyright (C) 2012 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.mail.browse;
19
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.database.DataSetObserver;
25import android.os.Bundle;
26import android.os.Parcelable;
27import android.support.v4.view.ViewPager;
28import android.view.ViewGroup;
29
30import com.android.mail.R;
31import com.android.mail.providers.Account;
32import com.android.mail.providers.Conversation;
33import com.android.mail.providers.Folder;
34import com.android.mail.providers.UIProvider;
35import com.android.mail.ui.ActivityController;
36import com.android.mail.ui.ConversationViewFragment;
37import com.android.mail.utils.FragmentStatePagerAdapter2;
38import com.android.mail.utils.LogTag;
39import com.android.mail.utils.LogUtils;
40import com.android.mail.utils.Utils;
41
42public class ConversationPagerAdapter extends FragmentStatePagerAdapter2 {
43
44    private final DataSetObserver mListObserver = new ListObserver();
45    private final DataSetObserver mFolderObserver = new FolderObserver();
46    private ActivityController mController;
47    private final Bundle mCommonFragmentArgs;
48    private final Conversation mInitialConversation;
49    private final Account mAccount;
50    private final Folder mFolder;
51    /**
52     * In singleton mode, this adapter ignores the cursor contents and size, and acts as if the
53     * data set size is exactly size=1, with {@link #getDefaultConversation()} at position 0.
54     */
55    private boolean mSingletonMode = true;
56    /**
57     * Similar to singleton mode, but once enabled, detached mode is permanent for this adapter.
58     */
59    private boolean mDetachedMode = false;
60    /**
61     * Adapter methods may trigger a data set change notification in the middle of a ViewPager
62     * update, but they are not safe to handle, so we have to ignore them. This will not ignore
63     * pager-external updates; it's impossible to be notified of an external change during
64     * an update.
65     *
66     * TODO: Queue up changes like this, if there ever are any that actually modify the data set.
67     * Right now there are none. Such a change would have to be of the form: instantiation or
68     * setPrimary somehow adds or removes items from the conversation cursor. Crazy!
69     */
70    private boolean mSafeToNotify;
71    /**
72     * Need to keep this around to look up pager title strings.
73     */
74    private Resources mResources;
75    /**
76     * This isn't great to create a circular dependency, but our usage of {@link #getPageTitle(int)}
77     * requires knowing which page is the currently visible to dynamically name offscreen pages
78     * "newer" and "older". And {@link #setPrimaryItem(ViewGroup, int, Object)} does not work well
79     * because it isn't updated as often as {@link ViewPager#getCurrentItem()} is.
80     * <p>
81     * We must be careful to null out this reference when the pager and adapter are decoupled to
82     * minimize dangling references.
83     */
84    private ViewPager mPager;
85
86    private static final String LOG_TAG = LogTag.getLogTag();
87
88    private static final String BUNDLE_DETACHED_MODE =
89            ConversationPagerAdapter.class.getName() + "-detachedmode";
90
91    public ConversationPagerAdapter(Resources res, FragmentManager fm, Account account,
92            Folder folder, Conversation initialConversation) {
93        super(fm, false /* enableSavedStates */);
94        mResources = res;
95        mCommonFragmentArgs = ConversationViewFragment.makeBasicArgs(account, folder);
96        mInitialConversation = initialConversation;
97        mAccount = account;
98        mFolder = folder;
99    }
100
101    public boolean matches(Account account, Folder folder) {
102        return mAccount != null && mFolder != null && mAccount.matches(account)
103                && mFolder.equals(folder);
104    }
105
106    public void setSingletonMode(boolean enabled) {
107        if (mSingletonMode != enabled) {
108            mSingletonMode = enabled;
109            notifyDataSetChanged();
110        }
111    }
112
113    public boolean isSingletonMode() {
114        return mSingletonMode;
115    }
116
117    public boolean isPagingDisabled() {
118        return mSingletonMode || mDetachedMode || getCursor() == null;
119    }
120
121    private Cursor getCursor() {
122        if (mController == null) {
123            // Happens when someone calls setActivityController(null) on us. This is done in
124            // ConversationPagerController.stopListening() to indicate that the Conversation View
125            // is going away *very* soon.
126            LogUtils.i(LOG_TAG, "Pager adapter has a null controller. If the conversation view"
127                    + " is going away, this is fine.  Otherwise, the state is inconsistent");
128            return null;
129        }
130
131        return mController.getConversationListCursor();
132    }
133
134    @Override
135    public Fragment getItem(int position) {
136        final Conversation c;
137
138        if (isPagingDisabled()) {
139            // cursor-less adapter is a size-1 cursor that points to mInitialConversation.
140            // sanity-check
141            if (position != 0) {
142                LogUtils.wtf(LOG_TAG, "pager cursor is null and position is non-zero: %d",
143                        position);
144            }
145            c = getDefaultConversation();
146            c.position = 0;
147        } else {
148            final Cursor cursor = getCursor();
149            if (cursor == null) {
150                LogUtils.wtf(LOG_TAG, "unable to get ConversationCursor, pos=%d", position);
151                return null;
152            }
153            if (!cursor.moveToPosition(position)) {
154                LogUtils.wtf(LOG_TAG, "unable to seek to ConversationCursor pos=%d (%s)", position,
155                        cursor);
156                return null;
157            }
158            // TODO: switch to something like MessageCursor or AttachmentCursor
159            // to re-use these models
160            c = new Conversation(cursor);
161            c.position = position;
162        }
163        final Fragment f = ConversationViewFragment.newInstance(mCommonFragmentArgs, c);
164        LogUtils.d(LOG_TAG, "IN PagerAdapter.getItem, frag=%s subj=%s", f, c.subject);
165        return f;
166    }
167
168    @Override
169    public int getCount() {
170        if (isPagingDisabled()) {
171            LogUtils.d(LOG_TAG, "IN CPA.getCount, returning 1 (effective singleton). cursor=%s",
172                    getCursor());
173            return 1;
174        }
175        final Cursor cursor = getCursor();
176        if (cursor == null) {
177            return 0;
178        }
179        return cursor.getCount();
180    }
181
182    @Override
183    public int getItemPosition(Object item) {
184        if (!(item instanceof ConversationViewFragment)) {
185            LogUtils.wtf(LOG_TAG, "getItemPosition received unexpected item: %s", item);
186        }
187
188        final ConversationViewFragment fragment = (ConversationViewFragment) item;
189        return getConversationPosition(fragment.getConversation());
190    }
191
192    @Override
193    public void setPrimaryItem(ViewGroup container, int position, Object object) {
194        LogUtils.d(LOG_TAG, "IN PagerAdapter.setPrimaryItem, pos=%d, frag=%s", position,
195                object);
196        super.setPrimaryItem(container, position, object);
197    }
198
199    @Override
200    public CharSequence getPageTitle(int position) {
201        final String title;
202        final int currentPosition = mPager.getCurrentItem();
203
204        if (isPagingDisabled()) {
205            title = null;
206        } else if (position == currentPosition) {
207            int total = getCount();
208            if (mController != null) {
209                final Folder f = mController.getFolder();
210                if (f != null && f.totalCount > total) {
211                    total = f.totalCount;
212                }
213            }
214            title = mResources.getString(R.string.conversation_count, position + 1, total);
215        } else {
216            title = mResources.getString(position < currentPosition ?
217                    R.string.conversation_newer : R.string.conversation_older);
218        }
219        return title;
220    }
221
222    @Override
223    public Parcelable saveState() {
224        LogUtils.d(LOG_TAG, "IN PagerAdapter.saveState. this=%s", this);
225        Bundle state = (Bundle) super.saveState(); // superclass uses a Bundle
226        if (state == null) {
227            state = new Bundle();
228        }
229        state.putBoolean(BUNDLE_DETACHED_MODE, mDetachedMode);
230        return state;
231    }
232
233    @Override
234    public void restoreState(Parcelable state, ClassLoader loader) {
235        LogUtils.d(LOG_TAG, "IN PagerAdapter.restoreState. this=%s", this);
236        super.restoreState(state, loader);
237        if (state != null) {
238            Bundle b = (Bundle) state;
239            b.setClassLoader(loader);
240            mDetachedMode = b.getBoolean(BUNDLE_DETACHED_MODE);
241        }
242    }
243
244    @Override
245    public void startUpdate(ViewGroup container) {
246        mSafeToNotify = false;
247        super.startUpdate(container);
248    }
249
250    @Override
251    public void finishUpdate(ViewGroup container) {
252        super.finishUpdate(container);
253        mSafeToNotify = true;
254    }
255
256    @Override
257    public void notifyDataSetChanged() {
258        if (!mSafeToNotify) {
259            LogUtils.d(LOG_TAG, "IN PagerAdapter.notifyDataSetChanged, ignoring unsafe update");
260            return;
261        }
262
263        // when the currently visible item disappears from the dataset:
264        //   if the new version of the currently visible item has zero messages:
265        //     notify the list controller so it can handle this 'current conversation gone' case
266        //     (by backing out of conversation mode)
267        //   else
268        //     'detach' the conversation view from the cursor, keeping the current item as-is but
269        //     disabling swipe (effectively the same as singleton mode)
270        if (mController != null) {
271            final Conversation currConversation = mController.getCurrentConversation();
272            final int pos = getConversationPosition(currConversation);
273            if (pos == POSITION_NONE) {
274                // enable detached mode and do no more here. the fragment itself will figure out
275                // if the conversation is empty (using message list cursor) and back out if needed.
276                mDetachedMode = true;
277                LogUtils.i(LOG_TAG, "CPA: current conv is gone, reverting to detached mode. c=%s",
278                        currConversation.uri);
279            }
280        }
281
282        super.notifyDataSetChanged();
283
284        // notify unaffected fragment items of the change, so they can re-render
285        // (the change may have been to the labels for a single conversation, for example)
286    }
287
288    @Override
289    public void setItemVisible(Fragment item, boolean visible) {
290        super.setItemVisible(item, visible);
291        final ConversationViewFragment fragment = (ConversationViewFragment) item;
292        fragment.setExtraUserVisibleHint(visible);
293
294        if (visible && mController != null) {
295            final Conversation c = fragment.getConversation();
296            LogUtils.d(LOG_TAG, "pager adapter setting current conv: %s (%s)", c.subject, item);
297            mController.setCurrentConversation(c);
298        }
299    }
300
301    private Conversation getDefaultConversation() {
302        Conversation c = (mController != null) ? mController.getCurrentConversation() : null;
303        if (c == null) {
304            c = mInitialConversation;
305        }
306        return c;
307    }
308
309    public int getConversationPosition(Conversation conv) {
310        if (isPagingDisabled()) {
311            if (getCursor() == null) {
312                return POSITION_NONE;
313            }
314
315            if (conv != getDefaultConversation()) {
316                LogUtils.d(LOG_TAG, "unable to find conversation in singleton mode. c=%s",
317                        conv);
318                return POSITION_NONE;
319            }
320            return 0;
321        }
322
323        final Cursor cursor = getCursor();
324        if (cursor == null) {
325            return POSITION_NONE;
326        }
327
328        final boolean networkWasEnabled = Utils.disableConversationCursorNetworkAccess(cursor);
329
330        int result = POSITION_NONE;
331        int pos = -1;
332        while (cursor.moveToPosition(++pos)) {
333            final long id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
334            if (conv.id == id) {
335                LogUtils.d(LOG_TAG, "pager adapter found repositioned convo '%s' at pos=%d",
336                        conv.subject, pos);
337                result = pos;
338                break;
339            }
340        }
341
342        if (networkWasEnabled) {
343            Utils.enableConversationCursorNetworkAccess(cursor);
344        }
345
346        return result;
347    }
348
349    public void setPager(ViewPager pager) {
350        mPager = pager;
351    }
352
353    public void setActivityController(ActivityController controller) {
354        if (mController != null) {
355            mController.unregisterConversationListObserver(mListObserver);
356            mController.unregisterFolderObserver(mFolderObserver);
357        }
358        mController = controller;
359        if (mController != null) {
360            mController.registerConversationListObserver(mListObserver);
361            mController.registerFolderObserver(mFolderObserver);
362
363            notifyDataSetChanged();
364        } else {
365            // We're being torn down; do not notify.
366            // Let the pager controller manage pager lifecycle.
367        }
368    }
369
370    // update the pager title strip as the Folder's conversation count changes
371    private class FolderObserver extends DataSetObserver {
372        @Override
373        public void onChanged() {
374            notifyDataSetChanged();
375        }
376    }
377
378    // update the pager dataset as the Controller's cursor changes
379    private class ListObserver extends DataSetObserver {
380        @Override
381        public void onChanged() {
382            notifyDataSetChanged();
383        }
384        @Override
385        public void onInvalidated() {
386        }
387    }
388
389}
390