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.animation.AnimatorListenerAdapter;
21import android.app.FragmentManager;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.database.DataSetObservable;
25import android.database.DataSetObserver;
26import android.graphics.drawable.Drawable;
27import android.support.v4.view.ViewPager;
28import android.view.View;
29import android.view.ViewPropertyAnimator;
30
31import com.android.mail.R;
32import com.android.mail.graphics.PageMarginDrawable;
33import com.android.mail.providers.Account;
34import com.android.mail.providers.Conversation;
35import com.android.mail.providers.Folder;
36import com.android.mail.ui.AbstractActivityController;
37import com.android.mail.ui.ActivityController;
38import com.android.mail.ui.RestrictedActivity;
39import com.android.mail.utils.LogUtils;
40import 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 */
60public 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
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     */
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     */
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     */
206    public void killRestoredFragments() {
207        mPagerAdapter.killRestoredFragments();
208    }
209
210    // Explicitly set the focus to the conversation pager, specifically the conv overlay.
211    public void focusPager() {
212        mPager.requestFocus();
213    }
214
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
223    public boolean isInitialConversationLoading() {
224        return mInitialConversationLoading;
225    }
226
227    public void onDestroy() {
228        // need to release resources before a configuration change kills the activity and controller
229        cleanup();
230    }
231
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
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
260    public void registerConversationLoadedObserver(DataSetObserver observer) {
261        mLoadedObservable.registerObserver(observer);
262    }
263
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     */
272    public void stopListening() {
273        if (mPagerAdapter != null) {
274            mPagerAdapter.stopListening();
275        }
276    }
277
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