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.animation.AnimatorListenerAdapter;
21 import android.app.FragmentManager;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.database.DataSetObservable;
25 import android.database.DataSetObserver;
26 import android.graphics.drawable.Drawable;
27 import android.support.v4.view.ViewPager;
28 import android.view.View;
29 import android.view.ViewPropertyAnimator;
30 
31 import com.android.mail.R;
32 import com.android.mail.graphics.PageMarginDrawable;
33 import com.android.mail.providers.Account;
34 import com.android.mail.providers.Conversation;
35 import com.android.mail.providers.Folder;
36 import com.android.mail.ui.AbstractActivityController;
37 import com.android.mail.ui.ActivityController;
38 import com.android.mail.ui.RestrictedActivity;
39 import com.android.mail.utils.LogUtils;
40 import com.android.mail.utils.Utils;
41 
42 /**
43  * A simple controller for a {@link ViewPager} of conversations.
44  * <p>
45  * Instead of placing a ViewPager in a Fragment that replaces the other app views, we leave a
46  * ViewPager in the activity's view hierarchy at all times and have this controller manage it.
47  * This allows the ViewPager to safely instantiate inner conversation fragments since it is not
48  * itself contained in a Fragment (no nested fragments!).
49  * <p>
50  * This arrangement has pros and cons...<br>
51  * pros: FragmentManager manages restoring conversation fragments, each conversation gets its own
52  * LoaderManager<br>
53  * cons: the activity's Controller has to specially handle show/hide conversation view,
54  * conversation fragment transitions must be done manually
55  * <p>
56  * This controller is a small delegate of {@link AbstractActivityController} and shares its
57  * lifetime.
58  *
59  */
60 public class ConversationPagerController {
61 
62     private ViewPager mPager;
63     private ConversationPagerAdapter mPagerAdapter;
64     private FragmentManager mFragmentManager;
65     private ActivityController mActivityController;
66     private boolean mShown;
67     /**
68      * True when the initial conversation passed to show() is busy loading. We assume that the
69      * first {@link #onConversationSeen()} callback is triggered by that initial
70      * conversation, and unset this flag when first signaled. Side-to-side paging will not re-enable
71      * this flag, since it's only needed for initial conversation load.
72      */
73     private boolean mInitialConversationLoading;
74     private final DataSetObservable mLoadedObservable = new DataSetObservable();
75 
76     public static final String LOG_TAG = "ConvPager";
77 
78     /**
79      * Enables an optimization to the PagerAdapter that causes ViewPager to initially load just the
80      * target conversation, then when the conversation view signals that the conversation is loaded
81      * and visible (via onConversationSeen), we switch to paged mode to load the left/right
82      * adjacent conversations.
83      * <p>
84      * Should improve load times. It also works around an issue in ViewPager that always loads item
85      * zero (with the fragment visibility hint ON) when the adapter is initially set.
86      */
87     private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = false;
88 
89     /** Duration of pager.show(...)'s animation */
90     private static final int SHOW_ANIMATION_DURATION = 300;
91 
ConversationPagerController(RestrictedActivity activity, ActivityController controller)92     public ConversationPagerController(RestrictedActivity activity,
93             ActivityController controller) {
94         mFragmentManager = activity.getFragmentManager();
95         mPager = (ViewPager) activity.findViewById(R.id.conversation_pager);
96         mActivityController = controller;
97         setupPageMargin(activity.getActivityContext());
98     }
99 
100     /**
101      * Show the conversation pager for the given conversation and animate in if specified along
102      * with given animation listener.
103      * @param account current account
104      * @param folder current folder
105      * @param initialConversation conversation to display initially in pager
106      * @param changeVisibility true if we need to make the pager appear
107      * @param pagerAnimationListener animation listener for pager fade-in, null indicates no
108      *                               animation should take place
109      */
show(Account account, Folder folder, Conversation initialConversation, boolean changeVisibility, AnimatorListenerAdapter pagerAnimationListener)110     public void show(Account account, Folder folder, Conversation initialConversation,
111             boolean changeVisibility, AnimatorListenerAdapter pagerAnimationListener) {
112         mInitialConversationLoading = true;
113 
114         if (mShown) {
115             LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
116             // optimize for the case where account+folder are the same, when we can just shift
117             // the existing pager to show the new conversation
118             // If in detached mode, don't do this optimization
119             if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)
120                     && !mPagerAdapter.isDetached()) {
121                 final int pos = mPagerAdapter.getConversationPosition(initialConversation);
122                 if (pos >= 0) {
123                     setCurrentItem(pos);
124                     return;
125                 }
126             }
127             // unable to shift, destroy existing state and fall through to normal startup
128             cleanup();
129         }
130 
131         if (changeVisibility) {
132             // If we have a pagerAnimationListener, go ahead and animate
133             if (pagerAnimationListener != null) {
134                 // Reset alpha to 0 before animating/making it visible
135                 mPager.setAlpha(0f);
136                 mPager.setVisibility(View.VISIBLE);
137 
138                 // Fade in pager to full visibility - this can be cancelled mid-animation
139                 mPager.animate().alpha(1f)
140                         .setDuration(SHOW_ANIMATION_DURATION).setListener(pagerAnimationListener);
141 
142             // Otherwise, make the pager appear without animation
143             } else {
144                 // In case pager animation was cancelled and alpha value was not reset,
145                 // ensure that the pager is completely visible for a non-animated pager.show
146                 mPager.setAlpha(1f);
147                 mPager.setVisibility(View.VISIBLE);
148             }
149         }
150 
151         mPagerAdapter = new ConversationPagerAdapter(mPager.getContext(), mFragmentManager,
152                 account, folder, initialConversation);
153         mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
154         mPagerAdapter.setActivityController(mActivityController);
155         mPagerAdapter.setPager(mPager);
156         LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
157 
158         Utils.sConvLoadTimer.mark("pager init");
159         LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s adapter=%s",
160                 mPagerAdapter.getCount(), initialConversation, mPagerAdapter);
161         mPager.setAdapter(mPagerAdapter);
162 
163         if (!ENABLE_SINGLETON_INITIAL_LOAD) {
164             // FIXME: unnecessary to do this on restore. setAdapter will restore current position
165             final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
166             if (initialPos >= 0) {
167                 LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
168                 setCurrentItem(initialPos);
169             }
170         }
171         Utils.sConvLoadTimer.mark("pager setAdapter");
172 
173         mShown = true;
174     }
175 
176     /**
177      * Hide the pager and cancel any running/pending animation
178      * @param changeVisibility true if we need to make the pager disappear
179      */
hide(boolean changeVisibility)180     public void hide(boolean changeVisibility) {
181         if (!mShown) {
182             LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
183             return;
184         }
185         mShown = false;
186 
187         // Cancel any potential animations to avoid listener methods running when they shouldn't
188         mPager.animate().cancel();
189 
190         if (changeVisibility) {
191             mPager.setVisibility(View.GONE);
192         }
193 
194         LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
195         mPager.setAdapter(null);
196         cleanup();
197     }
198 
199     /**
200      * Part of a delicate dance to kill fragments on restore after rotation if
201      * the device configuration no longer calls for them. You must call
202      * {@link #show(Account, Folder, Conversation, boolean, boolean)} first, and you probably want
203      * to call {@link #hide(boolean)} afterwards to finish the cleanup. See go/xqaxk. Sorry...
204      *
205      */
killRestoredFragments()206     public void killRestoredFragments() {
207         mPagerAdapter.killRestoredFragments();
208     }
209 
210     // Explicitly set the focus to the conversation pager, specifically the conv overlay.
focusPager()211     public void focusPager() {
212         mPager.requestFocus();
213     }
214 
setCurrentItem(int pos)215     private void setCurrentItem(int pos) {
216         // disable onPageSelected notifications during this operation. that listener is only there
217         // to update the rest of the app when the user swipes to another page.
218         mPagerAdapter.enablePageChangeListener(false);
219         mPager.setCurrentItem(pos);
220         mPagerAdapter.enablePageChangeListener(true);
221     }
222 
isInitialConversationLoading()223     public boolean isInitialConversationLoading() {
224         return mInitialConversationLoading;
225     }
226 
onDestroy()227     public void onDestroy() {
228         // need to release resources before a configuration change kills the activity and controller
229         cleanup();
230     }
231 
cleanup()232     private void cleanup() {
233         if (mPagerAdapter != null) {
234             // stop observing the conversation list
235             mPagerAdapter.setActivityController(null);
236             mPagerAdapter.setPager(null);
237             mPagerAdapter = null;
238         }
239     }
240 
onConversationSeen()241     public void onConversationSeen() {
242         if (mPagerAdapter == null) {
243             return;
244         }
245 
246         // take the adapter out of singleton mode to begin loading the
247         // other non-visible conversations
248         if (mPagerAdapter.isSingletonMode()) {
249             LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
250                     " switching to cursor mode to load other conversations");
251             mPagerAdapter.setSingletonMode(false);
252         }
253 
254         if (mInitialConversationLoading) {
255             mInitialConversationLoading = false;
256             mLoadedObservable.notifyChanged();
257         }
258     }
259 
registerConversationLoadedObserver(DataSetObserver observer)260     public void registerConversationLoadedObserver(DataSetObserver observer) {
261         mLoadedObservable.registerObserver(observer);
262     }
263 
unregisterConversationLoadedObserver(DataSetObserver observer)264     public void unregisterConversationLoadedObserver(DataSetObserver observer) {
265         mLoadedObservable.unregisterObserver(observer);
266     }
267 
268     /**
269      * Stops listening to changes to the adapter. This must be followed immediately by
270      * {@link #hide(boolean)}.
271      */
stopListening()272     public void stopListening() {
273         if (mPagerAdapter != null) {
274             mPagerAdapter.stopListening();
275         }
276     }
277 
setupPageMargin(Context c)278     private void setupPageMargin(Context c) {
279         final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
280         final Drawable divider = a.getDrawable(0);
281         a.recycle();
282         final int padding = c.getResources().getDimensionPixelOffset(
283                 R.dimen.conversation_page_gutter);
284         final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0,
285                 c.getResources().getColor(R.color.conversation_view_background_color));
286         mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
287         mPager.setPageMarginDrawable(gutterDrawable);
288     }
289 
290 }
291