• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.browse;
19 
20 import android.app.Fragment;
21 import android.app.FragmentManager;
22 import android.content.res.Resources;
23 import android.database.Cursor;
24 import android.database.DataSetObserver;
25 import android.os.Bundle;
26 import android.os.Parcelable;
27 import android.support.v4.view.ViewPager;
28 import android.view.ViewGroup;
29 
30 import com.android.mail.providers.Account;
31 import com.android.mail.providers.Conversation;
32 import com.android.mail.providers.Folder;
33 import com.android.mail.providers.FolderObserver;
34 import com.android.mail.providers.UIProvider;
35 import com.android.mail.ui.AbstractConversationViewFragment;
36 import com.android.mail.ui.ActivityController;
37 import com.android.mail.ui.ConversationViewFragment;
38 import com.android.mail.ui.SecureConversationViewFragment;
39 import com.android.mail.utils.FragmentStatePagerAdapter2;
40 import com.android.mail.utils.LogUtils;
41 
42 public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
43         implements ViewPager.OnPageChangeListener {
44 
45     private final DataSetObserver mListObserver = new ListObserver();
46     private final FolderObserver mFolderObserver = new FolderObserver() {
47         @Override
48         public void onChanged(Folder newFolder) {
49             notifyDataSetChanged();
50         }
51     };
52     private ActivityController mController;
53     private final Bundle mCommonFragmentArgs;
54     private final Conversation mInitialConversation;
55     private final Account mAccount;
56     private final Folder mFolder;
57     /**
58      * In singleton mode, this adapter ignores the cursor contents and size, and acts as if the
59      * data set size is exactly size=1, with {@link #getDefaultConversation()} at position 0.
60      */
61     private boolean mSingletonMode = false;
62     /**
63      * Similar to singleton mode, but once enabled, detached mode is permanent for this adapter.
64      */
65     private boolean mDetachedMode = false;
66     /**
67      * True iff we are in the process of handling a dataset change.
68      */
69     private boolean mInDataSetChange = false;
70     /**
71      * Need to keep this around to look up pager title strings.
72      */
73     private Resources mResources;
74     /**
75      * This isn't great to create a circular dependency, but our usage of {@link #getPageTitle(int)}
76      * requires knowing which page is the currently visible to dynamically name offscreen pages
77      * "newer" and "older". And {@link #setPrimaryItem(ViewGroup, int, Object)} does not work well
78      * because it isn't updated as often as {@link ViewPager#getCurrentItem()} is.
79      * <p>
80      * We must be careful to null out this reference when the pager and adapter are decoupled to
81      * minimize dangling references.
82      */
83     private ViewPager mPager;
84     private boolean mSanitizedHtml;
85 
86     private boolean mStopListeningMode = false;
87 
88     /**
89      * After {@link #stopListening()} is called, this contains the last-known count of this adapter.
90      * We keep this around and use it in lieu of the Cursor's true count until imminent destruction
91      * to satisfy two opposing requirements:
92      * <ol>
93      * <li>The ViewPager always likes to know about all dataset changes via notifyDatasetChanged.
94      * <li>Destructive changes during pager destruction (e.g. mode transition from conversation mode
95      * to list mode) must be ignored, or else ViewPager will shift focus onto a neighboring
96      * conversation and <b>mark it read</b>.
97      * </ol>
98      *
99      */
100     private int mLastKnownCount;
101 
102     private static final String LOG_TAG = ConversationPagerController.LOG_TAG;
103 
104     private static final String BUNDLE_DETACHED_MODE =
105             ConversationPagerAdapter.class.getName() + "-detachedmode";
106 
ConversationPagerAdapter(Resources res, FragmentManager fm, Account account, Folder folder, Conversation initialConversation)107     public ConversationPagerAdapter(Resources res, FragmentManager fm, Account account,
108             Folder folder, Conversation initialConversation) {
109         super(fm, false /* enableSavedStates */);
110         mResources = res;
111         mCommonFragmentArgs = AbstractConversationViewFragment.makeBasicArgs(account);
112         mInitialConversation = initialConversation;
113         mAccount = account;
114         mFolder = folder;
115         mSanitizedHtml = mAccount.supportsCapability
116                 (UIProvider.AccountCapabilities.SANITIZED_HTML);
117     }
118 
matches(Account account, Folder folder)119     public boolean matches(Account account, Folder folder) {
120         return mAccount != null && mFolder != null && mAccount.matches(account)
121                 && mFolder.equals(folder);
122     }
123 
setSingletonMode(boolean enabled)124     public void setSingletonMode(boolean enabled) {
125         if (mSingletonMode != enabled) {
126             mSingletonMode = enabled;
127             notifyDataSetChanged();
128         }
129     }
130 
isSingletonMode()131     public boolean isSingletonMode() {
132         return mSingletonMode;
133     }
134 
isDetached()135     public boolean isDetached() {
136         return mDetachedMode;
137     }
138 
139     /**
140      * Returns true if singleton mode or detached mode have been enabled, or if the current cursor
141      * is null.
142      * @param cursor the current conversation cursor (obtained through {@link #getCursor()}.
143      * @return
144      */
isPagingDisabled(Cursor cursor)145     public boolean isPagingDisabled(Cursor cursor) {
146         return mSingletonMode || mDetachedMode || cursor == null;
147     }
148 
getCursor()149     private ConversationCursor getCursor() {
150         if (mDetachedMode) {
151             // In detached mode, the pager is decoupled from the cursor. Nothing should rely on the
152             // cursor at this point.
153             return null;
154         }
155         if (mController == null) {
156             // Happens when someone calls setActivityController(null) on us. This is done in
157             // ConversationPagerController.stopListening() to indicate that the Conversation View
158             // is going away *very* soon.
159             LogUtils.i(LOG_TAG, "Pager adapter has a null controller. If the conversation view"
160                     + " is going away, this is fine.  Otherwise, the state is inconsistent");
161             return null;
162         }
163 
164         return mController.getConversationListCursor();
165     }
166 
167     @Override
getItem(int position)168     public Fragment getItem(int position) {
169         final Conversation c;
170         final ConversationCursor cursor = getCursor();
171 
172         if (isPagingDisabled(cursor)) {
173             // cursor-less adapter is a size-1 cursor that points to mInitialConversation.
174             // sanity-check
175             if (position != 0) {
176                 LogUtils.wtf(LOG_TAG, "pager cursor is null and position is non-zero: %d",
177                         position);
178             }
179             c = getDefaultConversation();
180             c.position = 0;
181         } else {
182             if (!cursor.moveToPosition(position)) {
183                 LogUtils.wtf(LOG_TAG, "unable to seek to ConversationCursor pos=%d (%s)", position,
184                         cursor);
185                 return null;
186             }
187             cursor.notifyUIPositionChange();
188             c = cursor.getConversation();
189             c.position = position;
190         }
191         final AbstractConversationViewFragment f = getConversationViewFragment(c);
192         LogUtils.d(LOG_TAG, "IN PagerAdapter.getItem, frag=%s conv=%s this=%s", f, c, this);
193         return f;
194     }
195 
getConversationViewFragment(Conversation c)196     private AbstractConversationViewFragment getConversationViewFragment(Conversation c) {
197         if (mSanitizedHtml) {
198             return ConversationViewFragment.newInstance(mCommonFragmentArgs, c);
199         } else {
200             return SecureConversationViewFragment.newInstance(mCommonFragmentArgs, c);
201         }
202     }
203 
204     @Override
getCount()205     public int getCount() {
206         if (mStopListeningMode) {
207             if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
208                 final Cursor cursor = getCursor();
209                 LogUtils.d(LOG_TAG,
210                         "IN CPA.getCount stopListeningMode, returning lastKnownCount=%d."
211                         + " cursor=%s real count=%s", mLastKnownCount, cursor,
212                         (cursor != null) ? cursor.getCount() : "N/A");
213             }
214             return mLastKnownCount;
215         }
216 
217         final Cursor cursor = getCursor();
218         if (isPagingDisabled(cursor)) {
219             LogUtils.d(LOG_TAG, "IN CPA.getCount, returning 1 (effective singleton). cursor=%s",
220                     cursor);
221             return 1;
222         }
223         return cursor.getCount();
224     }
225 
226     @Override
getItemPosition(Object item)227     public int getItemPosition(Object item) {
228         if (!(item instanceof AbstractConversationViewFragment)) {
229             LogUtils.wtf(LOG_TAG, "getItemPosition received unexpected item: %s", item);
230         }
231 
232         final AbstractConversationViewFragment fragment = (AbstractConversationViewFragment) item;
233         return getConversationPosition(fragment.getConversation());
234     }
235 
236     @Override
setPrimaryItem(ViewGroup container, int position, Object object)237     public void setPrimaryItem(ViewGroup container, int position, Object object) {
238         LogUtils.d(LOG_TAG, "IN PagerAdapter.setPrimaryItem, pos=%d, frag=%s", position,
239                 object);
240         super.setPrimaryItem(container, position, object);
241     }
242 
243     @Override
saveState()244     public Parcelable saveState() {
245         LogUtils.d(LOG_TAG, "IN PagerAdapter.saveState. this=%s", this);
246         Bundle state = (Bundle) super.saveState(); // superclass uses a Bundle
247         if (state == null) {
248             state = new Bundle();
249         }
250         state.putBoolean(BUNDLE_DETACHED_MODE, mDetachedMode);
251         return state;
252     }
253 
254     @Override
restoreState(Parcelable state, ClassLoader loader)255     public void restoreState(Parcelable state, ClassLoader loader) {
256         super.restoreState(state, loader);
257         if (state != null) {
258             Bundle b = (Bundle) state;
259             b.setClassLoader(loader);
260             final boolean detached = b.getBoolean(BUNDLE_DETACHED_MODE);
261             setDetachedMode(detached);
262         }
263         LogUtils.d(LOG_TAG, "OUT PagerAdapter.restoreState. this=%s", this);
264     }
265 
setDetachedMode(boolean detached)266     private void setDetachedMode(boolean detached) {
267         if (mDetachedMode == detached) {
268             return;
269         }
270         mDetachedMode = detached;
271         if (mDetachedMode) {
272             mController.setDetachedMode();
273         }
274         notifyDataSetChanged();
275     }
276 
277     @Override
toString()278     public String toString() {
279         final StringBuilder sb = new StringBuilder(super.toString());
280         sb.setLength(sb.length() - 1);
281         sb.append(" detachedMode=");
282         sb.append(mDetachedMode);
283         sb.append(" singletonMode=");
284         sb.append(mSingletonMode);
285         sb.append(" mController=");
286         sb.append(mController);
287         sb.append(" mPager=");
288         sb.append(mPager);
289         sb.append(" mStopListening=");
290         sb.append(mStopListeningMode);
291         sb.append(" mLastKnownCount=");
292         sb.append(mLastKnownCount);
293         sb.append(" cursor=");
294         sb.append(getCursor());
295         sb.append("}");
296         return sb.toString();
297     }
298 
299     @Override
notifyDataSetChanged()300     public void notifyDataSetChanged() {
301         if (mInDataSetChange) {
302             LogUtils.i(LOG_TAG, "CPA ignoring dataset change generated during dataset change");
303             return;
304         }
305 
306         mInDataSetChange = true;
307         // If we are in detached mode, changes to the cursor are of no interest to us, but they may
308         // be to parent classes.
309 
310         // when the currently visible item disappears from the dataset:
311         //   if the new version of the currently visible item has zero messages:
312         //     notify the list controller so it can handle this 'current conversation gone' case
313         //     (by backing out of conversation mode)
314         //   else
315         //     'detach' the conversation view from the cursor, keeping the current item as-is but
316         //     disabling swipe (effectively the same as singleton mode)
317         if (mController != null && !mDetachedMode && mPager != null) {
318             final Conversation currConversation = mController.getCurrentConversation();
319             final int pos = getConversationPosition(currConversation);
320             final ConversationCursor cursor = getCursor();
321             if (pos == POSITION_NONE && cursor != null && currConversation != null) {
322                 // enable detached mode and do no more here. the fragment itself will figure out
323                 // if the conversation is empty (using message list cursor) and back out if needed.
324                 setDetachedMode(true);
325                 LogUtils.i(LOG_TAG, "CPA: current conv is gone, reverting to detached mode. c=%s",
326                         currConversation.uri);
327 
328                 final int currentItem = mPager.getCurrentItem();
329 
330                 final AbstractConversationViewFragment fragment =
331                         (AbstractConversationViewFragment) getFragmentAt(currentItem);
332 
333                 if (fragment != null) {
334                     fragment.onDetachedModeEntered();
335                 } else {
336                     LogUtils.e(LOG_TAG,
337                             "CPA: notifyDataSetChanged: fragment null, current item: %d",
338                             currentItem);
339                 }
340             } else {
341                 // notify unaffected fragment items of the change, so they can re-render
342                 // (the change may have been to the labels for a single conversation, for example)
343                 final AbstractConversationViewFragment frag = (cursor == null) ? null :
344                         (AbstractConversationViewFragment) getFragmentAt(pos);
345                 if (frag != null && cursor.moveToPosition(pos) && frag.isUserVisible()) {
346                     // reload what we think is in the current position.
347                     final Conversation conv = cursor.getConversation();
348                     conv.position = pos;
349                     frag.onConversationUpdated(conv);
350                     mController.setCurrentConversation(conv);
351                 }
352             }
353         } else {
354             LogUtils.d(LOG_TAG, "in CPA.notifyDataSetChanged, doing nothing. this=%s", this);
355         }
356 
357         super.notifyDataSetChanged();
358         mInDataSetChange = false;
359     }
360 
361     @Override
setItemVisible(Fragment item, boolean visible)362     public void setItemVisible(Fragment item, boolean visible) {
363         super.setItemVisible(item, visible);
364         final AbstractConversationViewFragment fragment = (AbstractConversationViewFragment) item;
365         fragment.setExtraUserVisibleHint(visible);
366     }
367 
getDefaultConversation()368     private Conversation getDefaultConversation() {
369         Conversation c = (mController != null) ? mController.getCurrentConversation() : null;
370         if (c == null) {
371             c = mInitialConversation;
372         }
373         return c;
374     }
375 
getConversationPosition(Conversation conv)376     public int getConversationPosition(Conversation conv) {
377         if (conv == null) {
378             return POSITION_NONE;
379         }
380 
381         final ConversationCursor cursor = getCursor();
382         if (isPagingDisabled(cursor)) {
383             final Conversation def = getDefaultConversation();
384             if (!conv.equals(def)) {
385                 LogUtils.d(LOG_TAG, "unable to find conversation in singleton mode. c=%s def=%s",
386                         conv, def);
387                 return POSITION_NONE;
388             }
389             LogUtils.d(LOG_TAG, "in CPA.getConversationPosition returning 0, conv=%s this=%s",
390                     conv, this);
391             return 0;
392         }
393 
394         // cursor is guaranteed to be non-null because isPagingDisabled() above checks for null
395         // cursor.
396 
397         int result = POSITION_NONE;
398         final int pos = cursor.getConversationPosition(conv.id);
399         if (pos >= 0) {
400             LogUtils.d(LOG_TAG, "pager adapter found repositioned convo %s at pos=%d",
401                     conv, pos);
402             result = pos;
403         }
404 
405         LogUtils.d(LOG_TAG, "in CPA.getConversationPosition (normal), conv=%s pos=%s this=%s",
406                 conv, result, this);
407         return result;
408     }
409 
setPager(ViewPager pager)410     public void setPager(ViewPager pager) {
411         if (mPager != null) {
412             mPager.setOnPageChangeListener(null);
413         }
414         mPager = pager;
415         if (mPager != null) {
416             mPager.setOnPageChangeListener(this);
417         }
418     }
419 
setActivityController(ActivityController controller)420     public void setActivityController(ActivityController controller) {
421         boolean wasNull = (mController == null);
422         if (mController != null && !mStopListeningMode) {
423             mController.unregisterConversationListObserver(mListObserver);
424             mController.unregisterFolderObserver(mFolderObserver);
425         }
426         mController = controller;
427         if (mController != null && !mStopListeningMode) {
428             mController.registerConversationListObserver(mListObserver);
429             mFolderObserver.initialize(mController);
430             if (!wasNull) {
431                 notifyDataSetChanged();
432             }
433         } else {
434             // We're being torn down; do not notify.
435             // Let the pager controller manage pager lifecycle.
436         }
437     }
438 
439     /**
440      * See {@link ConversationPagerController#stopListening()}.
441      */
stopListening()442     public void stopListening() {
443         if (mStopListeningMode) {
444             // Do nothing since we're already in stop listening mode.  This avoids repeated
445             // unregister observer calls.
446             return;
447         }
448 
449         // disable the observer, but save off the current count, in case the Pager asks for it
450         // from now until imminent destruction
451 
452         if (mController != null) {
453             mController.unregisterConversationListObserver(mListObserver);
454             mFolderObserver.unregisterAndDestroy();
455         }
456         mLastKnownCount = getCount();
457         mStopListeningMode = true;
458         LogUtils.d(LOG_TAG, "CPA.stopListening, this=%s", this);
459     }
460 
461     @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)462     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
463         // no-op
464     }
465 
466     @Override
onPageSelected(int position)467     public void onPageSelected(int position) {
468         if (mController == null) {
469             return;
470         }
471         final ConversationCursor cursor = getCursor();
472         if (cursor == null || !cursor.moveToPosition(position)) {
473             // No valid cursor or it doesn't have the position we want. Bail.
474             return;
475         }
476         final Conversation c = cursor.getConversation();
477         c.position = position;
478         LogUtils.d(LOG_TAG, "pager adapter setting current conv: %s", c);
479         mController.setCurrentConversation(c);
480     }
481 
482     @Override
onPageScrollStateChanged(int state)483     public void onPageScrollStateChanged(int state) {
484         // no-op
485     }
486 
487     // update the pager dataset as the Controller's cursor changes
488     private class ListObserver extends DataSetObserver {
489         @Override
onChanged()490         public void onChanged() {
491             notifyDataSetChanged();
492         }
493         @Override
onInvalidated()494         public void onInvalidated() {
495         }
496     }
497 
498 }
499