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(Conversation)} 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_pane); 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 public boolean isInitialConversationLoading() { 161 return mInitialConversationLoading; 162 } 163 164 public void onDestroy() { 165 // need to release resources before a configuration change kills the activity and controller 166 cleanup(); 167 } 168 169 private void cleanup() { 170 if (mPagerAdapter != null) { 171 // stop observing the conversation list 172 mPagerAdapter.setActivityController(null); 173 mPagerAdapter.setPager(null); 174 mPagerAdapter = null; 175 } 176 } 177 178 public void onConversationSeen() { 179 if (mPagerAdapter == null) { 180 return; 181 } 182 183 // take the adapter out of singleton mode to begin loading the 184 // other non-visible conversations 185 if (mPagerAdapter.isSingletonMode()) { 186 LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," + 187 " switching to cursor mode to load other conversations"); 188 mPagerAdapter.setSingletonMode(false); 189 } 190 191 if (mInitialConversationLoading) { 192 mInitialConversationLoading = false; 193 mLoadedObservable.notifyChanged(); 194 } 195 } 196 197 public void registerConversationLoadedObserver(DataSetObserver observer) { 198 mLoadedObservable.registerObserver(observer); 199 } 200 201 public void unregisterConversationLoadedObserver(DataSetObserver observer) { 202 mLoadedObservable.unregisterObserver(observer); 203 } 204 205 /** 206 * Stops listening to changes to the adapter. This must be followed immediately by 207 * {@link #hide(boolean)}. 208 */ 209 public void stopListening() { 210 if (mPagerAdapter != null) { 211 mPagerAdapter.stopListening(); 212 } 213 } 214 215 private void setupPageMargin(Context c) { 216 final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider}); 217 final Drawable divider = a.getDrawable(0); 218 a.recycle(); 219 final int padding = c.getResources().getDimensionPixelOffset( 220 R.dimen.conversation_page_gutter); 221 final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0, 222 c.getResources().getColor(R.color.conversation_view_border_color)); 223 mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding); 224 mPager.setPageMarginDrawable(gutterDrawable); 225 } 226 227} 228