ConversationPagerController.java revision 68459a7b3265da5002ff331529f12f6b408c9b03
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.Fragment;
21import android.app.FragmentManager;
22import android.support.v4.view.ViewPager;
23import android.support.v4.view.ViewPager.OnPageChangeListener;
24import android.view.View;
25
26import com.android.mail.R;
27import com.android.mail.providers.Account;
28import com.android.mail.providers.Conversation;
29import com.android.mail.providers.Folder;
30import com.android.mail.ui.AbstractActivityController;
31import com.android.mail.ui.ActivityController;
32import com.android.mail.ui.RestrictedActivity;
33import com.android.mail.ui.SubjectDisplayChanger;
34import com.android.mail.utils.LogTag;
35import com.android.mail.utils.LogUtils;
36
37/**
38 * A simple controller for a {@link ViewPager} of conversations.
39 * <p>
40 * Instead of placing a ViewPager in a Fragment that replaces the other app views, we leave a
41 * ViewPager in the activity's view hierarchy at all times and have this controller manage it.
42 * This allows the ViewPager to safely instantiate inner conversation fragments since it is not
43 * itself contained in a Fragment (no nested fragments!).
44 * <p>
45 * This arrangement has pros and cons...<br>
46 * pros: FragmentManager manages restoring conversation fragments, each conversation gets its own
47 * LoaderManager<br>
48 * cons: the activity's Controller has to specially handle show/hide conversation view,
49 * conversation fragment transitions must be done manually
50 * <p>
51 * This controller is a small delegate of {@link AbstractActivityController} and shares its
52 * lifetime.
53 *
54 */
55public class ConversationPagerController implements OnPageChangeListener {
56
57    private ViewPager mPager;
58    private ConversationPagerAdapter mPagerAdapter;
59    private FragmentManager mFragmentManager;
60    private ActivityController mActivityController;
61    private SubjectDisplayChanger mSubjectDisplayChanger;
62    private boolean mShown;
63
64    private static final String LOG_TAG = LogTag.getLogTag();
65
66    /**
67     * Enables an optimization to the PagerAdapter that causes ViewPager to initially load just the
68     * target conversation, then when the conversation view signals that the conversation is loaded
69     * and visible (via onConversationSeen), we switch to paged mode to load the left/right
70     * adjacent conversations.
71     * <p>
72     * Should improve load times. It also works around an issue in ViewPager that always loads item
73     * zero (with the fragment visibility hint ON) when the adapter is initially set.
74     */
75    private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = true;
76
77    public ConversationPagerController(RestrictedActivity activity,
78            ActivityController controller) {
79        mFragmentManager = activity.getFragmentManager();
80        mPager = (ViewPager) activity.findViewById(R.id.conversation_pane);
81        mActivityController = controller;
82        mSubjectDisplayChanger = controller.getSubjectDisplayChanger();
83    }
84
85    public void show(Account account, Folder folder, Conversation initialConversation) {
86        if (mShown) {
87            LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
88            // optimize for the case where account+folder are the same, when we can just shift
89            // the existing pager to show the new conversation
90            if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)) {
91                final int pos = mPagerAdapter.getConversationPosition(initialConversation);
92                if (pos >= 0) {
93                    mPager.setCurrentItem(pos);
94                    return;
95                }
96            }
97            // unable to shift, destroy existing state and fall through to normal startup
98            cleanup();
99        }
100
101        mPager.setVisibility(View.VISIBLE);
102
103        mPagerAdapter = new ConversationPagerAdapter(mPager.getResources(), mFragmentManager,
104                account, folder, initialConversation);
105        mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
106        mPagerAdapter.setActivityController(mActivityController);
107        mPagerAdapter.setPager(mPager);
108        LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
109
110        mPager.setOnPageChangeListener(this);
111
112        LogUtils.d(LOG_TAG, "init pager adapter, count=%d initial=%s", mPagerAdapter.getCount(),
113                initialConversation.subject);
114        mPager.setAdapter(mPagerAdapter);
115
116        if (!ENABLE_SINGLETON_INITIAL_LOAD) {
117            // FIXME: unnecessary to do this on restore. setAdapter will restore current position
118            final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
119            LogUtils.w(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
120            mPager.setCurrentItem(initialPos);
121        }
122
123        mShown = true;
124    }
125
126    public void hide() {
127        if (!mShown) {
128            LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
129            return;
130        }
131        mShown = false;
132        mPager.setVisibility(View.GONE);
133
134        mSubjectDisplayChanger.clearSubject();
135
136        LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
137        mPager.setAdapter(null);
138        mPager.setOnPageChangeListener(null);
139        cleanup();
140    }
141
142    public void onDestroy() {
143        // need to release resources before a configuration change kills the activity and controller
144        cleanup();
145    }
146
147    private void cleanup() {
148        if (mPagerAdapter != null) {
149            // stop observing the conversation list
150            mPagerAdapter.setActivityController(null);
151            mPagerAdapter.setPager(null);
152            mPagerAdapter = null;
153        }
154    }
155
156    public void onConversationSeen(Conversation conv) {
157        // take the adapter out of singleton mode to begin loading the
158        // other non-visible conversations
159        if (mPagerAdapter != null && mPagerAdapter.isSingletonMode()) {
160            LogUtils.d(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
161                    " switching to cursor mode to load other conversations");
162            mPagerAdapter.setSingletonMode(false);
163        }
164    }
165
166    @Override
167    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
168        // no-op
169    }
170
171    @Override
172    public void onPageSelected(int position) {
173        final Fragment f = mPagerAdapter.getFragmentAt(position);
174        if (f != null) {
175            mPagerAdapter.setItemVisible(f, true);
176        }
177    }
178
179    @Override
180    public void onPageScrollStateChanged(int state) {
181        // no-op
182    }
183
184    /**
185     * Stops listening to changes to the adapter. This must be followed immediately by
186     * {@link #hide()}.
187     */
188    public void stopListening() {
189        mPagerAdapter.setActivityController(null);
190    }
191}
192