blob: ab81016d876b299b804c0cf7b3cdec40df44ee45 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.browse;
import android.app.FragmentManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.support.v4.view.ViewPager;
import android.view.View;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.ui.AbstractActivityController;
import com.android.mail.ui.ActivityController;
import com.android.mail.ui.RestrictedActivity;
import com.android.mail.ui.SubjectDisplayChanger;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
/**
* A simple controller for a {@link ViewPager} of conversations.
* <p>
* Instead of placing a ViewPager in a Fragment that replaces the other app views, we leave a
* ViewPager in the activity's view hierarchy at all times and have this controller manage it.
* This allows the ViewPager to safely instantiate inner conversation fragments since it is not
* itself contained in a Fragment (no nested fragments!).
* <p>
* This arrangement has pros and cons...<br>
* pros: FragmentManager manages restoring conversation fragments, each conversation gets its own
* LoaderManager<br>
* cons: the activity's Controller has to specially handle show/hide conversation view,
* conversation fragment transitions must be done manually
* <p>
* This controller is a small delegate of {@link AbstractActivityController} and shares its
* lifetime.
*
*/
public class ConversationPagerController {
private ViewPager mPager;
private ConversationPagerAdapter mPagerAdapter;
private FragmentManager mFragmentManager;
private ActivityController mActivityController;
private SubjectDisplayChanger mSubjectDisplayChanger;
private boolean mShown;
/**
* True when the initial conversation passed to show() is busy loading. We assume that the
* first {@link #onConversationSeen(Conversation)} callback is triggered by that initial
* conversation, and unset this flag when first signaled. Side-to-side paging will not re-enable
* this flag, since it's only needed for initial conversation load.
*/
private boolean mInitialConversationLoading;
private final DataSetObservable mLoadedObservable = new DataSetObservable();
private static final String LOG_TAG = LogTag.getLogTag();
/**
* Enables an optimization to the PagerAdapter that causes ViewPager to initially load just the
* target conversation, then when the conversation view signals that the conversation is loaded
* and visible (via onConversationSeen), we switch to paged mode to load the left/right
* adjacent conversations.
* <p>
* Should improve load times. It also works around an issue in ViewPager that always loads item
* zero (with the fragment visibility hint ON) when the adapter is initially set.
*/
private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = false;
public ConversationPagerController(RestrictedActivity activity,
ActivityController controller) {
mFragmentManager = activity.getFragmentManager();
mPager = (ViewPager) activity.findViewById(R.id.conversation_pane);
mActivityController = controller;
mSubjectDisplayChanger = controller.getSubjectDisplayChanger();
setupPageMargin(activity.getActivityContext());
}
public void show(Account account, Folder folder, Conversation initialConversation,
boolean changeVisibility) {
mInitialConversationLoading = true;
if (mShown) {
LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
// optimize for the case where account+folder are the same, when we can just shift
// the existing pager to show the new conversation
// If in detached mode, don't do this optimization
if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)
&& !mPagerAdapter.isDetached()) {
final int pos = mPagerAdapter.getConversationPosition(initialConversation);
if (pos >= 0) {
mPager.setCurrentItem(pos);
return;
}
}
// unable to shift, destroy existing state and fall through to normal startup
cleanup();
}
if (changeVisibility) {
mPager.setVisibility(View.VISIBLE);
}
mPagerAdapter = new ConversationPagerAdapter(mPager.getResources(), mFragmentManager,
account, folder, initialConversation);
mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
mPagerAdapter.setActivityController(mActivityController);
mPagerAdapter.setPager(mPager);
LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
Utils.sConvLoadTimer.mark("pager init");
LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s", mPagerAdapter.getCount(),
initialConversation);
mPager.setAdapter(mPagerAdapter);
if (!ENABLE_SINGLETON_INITIAL_LOAD) {
// FIXME: unnecessary to do this on restore. setAdapter will restore current position
final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
if (initialPos >= 0) {
LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
mPager.setCurrentItem(initialPos);
}
}
Utils.sConvLoadTimer.mark("pager setAdapter");
mShown = true;
}
public void hide(boolean changeVisibility) {
if (!mShown) {
LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
return;
}
mShown = false;
if (changeVisibility) {
mPager.setVisibility(View.GONE);
}
mSubjectDisplayChanger.clearSubjectAndUpdate();
LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
mPager.setAdapter(null);
cleanup();
}
public boolean isInitialConversationLoading() {
return mInitialConversationLoading;
}
public void onDestroy() {
// need to release resources before a configuration change kills the activity and controller
cleanup();
}
private void cleanup() {
if (mPagerAdapter != null) {
// stop observing the conversation list
mPagerAdapter.setActivityController(null);
mPagerAdapter.setPager(null);
mPagerAdapter = null;
}
}
public void onConversationSeen(Conversation conv) {
if (mPagerAdapter == null) {
return;
}
// take the adapter out of singleton mode to begin loading the
// other non-visible conversations
if (mPagerAdapter.isSingletonMode()) {
LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
" switching to cursor mode to load other conversations");
mPagerAdapter.setSingletonMode(false);
}
if (mInitialConversationLoading) {
mInitialConversationLoading = false;
mLoadedObservable.notifyChanged();
}
}
public void registerConversationLoadedObserver(DataSetObserver observer) {
mLoadedObservable.registerObserver(observer);
}
public void unregisterConversationLoadedObserver(DataSetObserver observer) {
mLoadedObservable.unregisterObserver(observer);
}
/**
* Stops listening to changes to the adapter. This must be followed immediately by
* {@link #hide(boolean)}.
*/
public void stopListening() {
if (mPagerAdapter != null) {
mPagerAdapter.stopListening();
}
}
private void setupPageMargin(Context c) {
final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
final Drawable divider = a.getDrawable(0);
a.recycle();
final int padding = c.getResources().getDimensionPixelOffset(
R.dimen.conversation_page_gutter);
final Drawable gutterDrawable = new InsetDrawable(divider, padding, 0, padding, 0);
mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
mPager.setPageMarginDrawable(gutterDrawable);
}
}