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.FragmentManager;
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.database.DataSetObservable;
     24 import android.database.DataSetObserver;
     25 import android.graphics.drawable.Drawable;
     26 import android.support.v4.view.ViewPager;
     27 import android.view.View;
     28 
     29 import com.android.mail.R;
     30 import com.android.mail.graphics.PageMarginDrawable;
     31 import com.android.mail.providers.Account;
     32 import com.android.mail.providers.Conversation;
     33 import com.android.mail.providers.Folder;
     34 import com.android.mail.ui.AbstractActivityController;
     35 import com.android.mail.ui.ActivityController;
     36 import com.android.mail.ui.RestrictedActivity;
     37 import com.android.mail.utils.LogUtils;
     38 import com.android.mail.utils.Utils;
     39 
     40 /**
     41  * A simple controller for a {@link ViewPager} of conversations.
     42  * <p>
     43  * Instead of placing a ViewPager in a Fragment that replaces the other app views, we leave a
     44  * ViewPager in the activity's view hierarchy at all times and have this controller manage it.
     45  * This allows the ViewPager to safely instantiate inner conversation fragments since it is not
     46  * itself contained in a Fragment (no nested fragments!).
     47  * <p>
     48  * This arrangement has pros and cons...<br>
     49  * pros: FragmentManager manages restoring conversation fragments, each conversation gets its own
     50  * LoaderManager<br>
     51  * cons: the activity's Controller has to specially handle show/hide conversation view,
     52  * conversation fragment transitions must be done manually
     53  * <p>
     54  * This controller is a small delegate of {@link AbstractActivityController} and shares its
     55  * lifetime.
     56  *
     57  */
     58 public class ConversationPagerController {
     59 
     60     private ViewPager mPager;
     61     private ConversationPagerAdapter mPagerAdapter;
     62     private FragmentManager mFragmentManager;
     63     private ActivityController mActivityController;
     64     private boolean mShown;
     65     /**
     66      * True when the initial conversation passed to show() is busy loading. We assume that the
     67      * first {@link #onConversationSeen(Conversation)} callback is triggered by that initial
     68      * conversation, and unset this flag when first signaled. Side-to-side paging will not re-enable
     69      * this flag, since it's only needed for initial conversation load.
     70      */
     71     private boolean mInitialConversationLoading;
     72     private final DataSetObservable mLoadedObservable = new DataSetObservable();
     73 
     74     public static final String LOG_TAG = "ConvPager";
     75 
     76     /**
     77      * Enables an optimization to the PagerAdapter that causes ViewPager to initially load just the
     78      * target conversation, then when the conversation view signals that the conversation is loaded
     79      * and visible (via onConversationSeen), we switch to paged mode to load the left/right
     80      * adjacent conversations.
     81      * <p>
     82      * Should improve load times. It also works around an issue in ViewPager that always loads item
     83      * zero (with the fragment visibility hint ON) when the adapter is initially set.
     84      */
     85     private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = false;
     86 
     87     public ConversationPagerController(RestrictedActivity activity,
     88             ActivityController controller) {
     89         mFragmentManager = activity.getFragmentManager();
     90         mPager = (ViewPager) activity.findViewById(R.id.conversation_pane);
     91         mActivityController = controller;
     92         setupPageMargin(activity.getActivityContext());
     93     }
     94 
     95     public void show(Account account, Folder folder, Conversation initialConversation,
     96             boolean changeVisibility) {
     97         mInitialConversationLoading = true;
     98 
     99         if (mShown) {
    100             LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
    101             // optimize for the case where account+folder are the same, when we can just shift
    102             // the existing pager to show the new conversation
    103             // If in detached mode, don't do this optimization
    104             if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)
    105                     && !mPagerAdapter.isDetached()) {
    106                 final int pos = mPagerAdapter.getConversationPosition(initialConversation);
    107                 if (pos >= 0) {
    108                     mPager.setCurrentItem(pos);
    109                     return;
    110                 }
    111             }
    112             // unable to shift, destroy existing state and fall through to normal startup
    113             cleanup();
    114         }
    115 
    116         if (changeVisibility) {
    117             mPager.setVisibility(View.VISIBLE);
    118         }
    119 
    120         mPagerAdapter = new ConversationPagerAdapter(mPager.getResources(), mFragmentManager,
    121                 account, folder, initialConversation);
    122         mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
    123         mPagerAdapter.setActivityController(mActivityController);
    124         mPagerAdapter.setPager(mPager);
    125         LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
    126 
    127         Utils.sConvLoadTimer.mark("pager init");
    128         LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s adapter=%s",
    129                 mPagerAdapter.getCount(), initialConversation, mPagerAdapter);
    130         mPager.setAdapter(mPagerAdapter);
    131 
    132         if (!ENABLE_SINGLETON_INITIAL_LOAD) {
    133             // FIXME: unnecessary to do this on restore. setAdapter will restore current position
    134             final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
    135             if (initialPos >= 0) {
    136                 LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
    137                 mPager.setCurrentItem(initialPos);
    138             }
    139         }
    140         Utils.sConvLoadTimer.mark("pager setAdapter");
    141 
    142         mShown = true;
    143     }
    144 
    145     public void hide(boolean changeVisibility) {
    146         if (!mShown) {
    147             LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
    148             return;
    149         }
    150         mShown = false;
    151         if (changeVisibility) {
    152             mPager.setVisibility(View.GONE);
    153         }
    154 
    155         LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
    156         mPager.setAdapter(null);
    157         cleanup();
    158     }
    159 
    160     public boolean isInitialConversationLoading() {
    161         return mInitialConversationLoading;
    162     }
    163 
    164     public void onDestroy() {
    165         // need to release resources before a configuration change kills the activity and controller
    166         cleanup();
    167     }
    168 
    169     private void cleanup() {
    170         if (mPagerAdapter != null) {
    171             // stop observing the conversation list
    172             mPagerAdapter.setActivityController(null);
    173             mPagerAdapter.setPager(null);
    174             mPagerAdapter = null;
    175         }
    176     }
    177 
    178     public void onConversationSeen() {
    179         if (mPagerAdapter == null) {
    180             return;
    181         }
    182 
    183         // take the adapter out of singleton mode to begin loading the
    184         // other non-visible conversations
    185         if (mPagerAdapter.isSingletonMode()) {
    186             LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
    187                     " switching to cursor mode to load other conversations");
    188             mPagerAdapter.setSingletonMode(false);
    189         }
    190 
    191         if (mInitialConversationLoading) {
    192             mInitialConversationLoading = false;
    193             mLoadedObservable.notifyChanged();
    194         }
    195     }
    196 
    197     public void registerConversationLoadedObserver(DataSetObserver observer) {
    198         mLoadedObservable.registerObserver(observer);
    199     }
    200 
    201     public void unregisterConversationLoadedObserver(DataSetObserver observer) {
    202         mLoadedObservable.unregisterObserver(observer);
    203     }
    204 
    205     /**
    206      * Stops listening to changes to the adapter. This must be followed immediately by
    207      * {@link #hide(boolean)}.
    208      */
    209     public void stopListening() {
    210         if (mPagerAdapter != null) {
    211             mPagerAdapter.stopListening();
    212         }
    213     }
    214 
    215     private void setupPageMargin(Context c) {
    216         final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
    217         final Drawable divider = a.getDrawable(0);
    218         a.recycle();
    219         final int padding = c.getResources().getDimensionPixelOffset(
    220                 R.dimen.conversation_page_gutter);
    221         final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0,
    222                 c.getResources().getColor(R.color.conversation_view_border_color));
    223         mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
    224         mPager.setPageMarginDrawable(gutterDrawable);
    225     }
    226 
    227 }
    228