Home | History | Annotate | Download | only in browse
      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 
    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 
    119     public boolean matches(Account account, Folder folder) {
    120         return mAccount != null && mFolder != null && mAccount.matches(account)
    121                 && mFolder.equals(folder);
    122     }
    123 
    124     public void setSingletonMode(boolean enabled) {
    125         if (mSingletonMode != enabled) {
    126             mSingletonMode = enabled;
    127             notifyDataSetChanged();
    128         }
    129     }
    130 
    131     public boolean isSingletonMode() {
    132         return mSingletonMode;
    133     }
    134 
    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      */
    145     public boolean isPagingDisabled(Cursor cursor) {
    146         return mSingletonMode || mDetachedMode || cursor == null;
    147     }
    148 
    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
    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 
    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
    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
    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
    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
    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
    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 
    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
    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
    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
    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 
    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 
    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 
    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 
    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      */
    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
    462     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    463         // no-op
    464     }
    465 
    466     @Override
    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
    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
    490         public void onChanged() {
    491             notifyDataSetChanged();
    492         }
    493         @Override
    494         public void onInvalidated() {
    495         }
    496     }
    497 
    498 }
    499