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.app.FragmentManager;
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.database.DataSetObservable;
24import android.database.DataSetObserver;
25import android.graphics.drawable.Drawable;
26import android.support.v4.view.ViewPager;
27import android.view.View;
28
29import com.android.mail.R;
30import com.android.mail.graphics.PageMarginDrawable;
31import com.android.mail.providers.Account;
32import com.android.mail.providers.Conversation;
33import com.android.mail.providers.Folder;
34import com.android.mail.ui.AbstractActivityController;
35import com.android.mail.ui.ActivityController;
36import com.android.mail.ui.RestrictedActivity;
37import com.android.mail.utils.LogUtils;
38import 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 */
58public 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()} 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_pager);
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    // Explicitly set the focus to the conversation pager, specifically the conv overlay.
161    public void focusPager() {
162        mPager.requestFocus();
163    }
164
165    public boolean isInitialConversationLoading() {
166        return mInitialConversationLoading;
167    }
168
169    public void onDestroy() {
170        // need to release resources before a configuration change kills the activity and controller
171        cleanup();
172    }
173
174    private void cleanup() {
175        if (mPagerAdapter != null) {
176            // stop observing the conversation list
177            mPagerAdapter.setActivityController(null);
178            mPagerAdapter.setPager(null);
179            mPagerAdapter = null;
180        }
181    }
182
183    public void onConversationSeen() {
184        if (mPagerAdapter == null) {
185            return;
186        }
187
188        // take the adapter out of singleton mode to begin loading the
189        // other non-visible conversations
190        if (mPagerAdapter.isSingletonMode()) {
191            LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
192                    " switching to cursor mode to load other conversations");
193            mPagerAdapter.setSingletonMode(false);
194        }
195
196        if (mInitialConversationLoading) {
197            mInitialConversationLoading = false;
198            mLoadedObservable.notifyChanged();
199        }
200    }
201
202    public void registerConversationLoadedObserver(DataSetObserver observer) {
203        mLoadedObservable.registerObserver(observer);
204    }
205
206    public void unregisterConversationLoadedObserver(DataSetObserver observer) {
207        mLoadedObservable.unregisterObserver(observer);
208    }
209
210    /**
211     * Stops listening to changes to the adapter. This must be followed immediately by
212     * {@link #hide(boolean)}.
213     */
214    public void stopListening() {
215        if (mPagerAdapter != null) {
216            mPagerAdapter.stopListening();
217        }
218    }
219
220    private void setupPageMargin(Context c) {
221        final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
222        final Drawable divider = a.getDrawable(0);
223        a.recycle();
224        final int padding = c.getResources().getDimensionPixelOffset(
225                R.dimen.conversation_page_gutter);
226        final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0,
227                c.getResources().getColor(R.color.conversation_view_background_color));
228        mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
229        mPager.setPageMarginDrawable(gutterDrawable);
230    }
231
232}
233