ConversationPagerAdapter.java revision 3825f3d2284b2b57fadcfe6a4ebd9992f3c5c7bb
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.content.res.Resources; 23import android.database.Cursor; 24import android.database.DataSetObserver; 25import android.os.Bundle; 26import android.os.Parcelable; 27import android.support.v4.view.ViewPager; 28import android.view.ViewGroup; 29 30import com.android.mail.R; 31import com.android.mail.providers.Account; 32import com.android.mail.providers.Conversation; 33import com.android.mail.providers.Folder; 34import com.android.mail.providers.UIProvider; 35import com.android.mail.ui.ActivityController; 36import com.android.mail.ui.ConversationViewFragment; 37import com.android.mail.utils.FragmentStatePagerAdapter2; 38import com.android.mail.utils.LogTag; 39import com.android.mail.utils.LogUtils; 40import com.android.mail.utils.Utils; 41 42public class ConversationPagerAdapter extends FragmentStatePagerAdapter2 { 43 44 private final DataSetObserver mListObserver = new ListObserver(); 45 private final DataSetObserver mFolderObserver = new FolderObserver(); 46 private ActivityController mController; 47 private final Bundle mCommonFragmentArgs; 48 private final Conversation mInitialConversation; 49 private final Account mAccount; 50 private final Folder mFolder; 51 /** 52 * In singleton mode, this adapter ignores the cursor contents and size, and acts as if the 53 * data set size is exactly size=1, with {@link #getDefaultConversation()} at position 0. 54 */ 55 private boolean mSingletonMode = true; 56 /** 57 * Similar to singleton mode, but once enabled, detached mode is permanent for this adapter. 58 */ 59 private boolean mDetachedMode = false; 60 /** 61 * Adapter methods may trigger a data set change notification in the middle of a ViewPager 62 * update, but they are not safe to handle, so we have to ignore them. This will not ignore 63 * pager-external updates; it's impossible to be notified of an external change during 64 * an update. 65 * 66 * TODO: Queue up changes like this, if there ever are any that actually modify the data set. 67 * Right now there are none. Such a change would have to be of the form: instantiation or 68 * setPrimary somehow adds or removes items from the conversation cursor. Crazy! 69 */ 70 private boolean mSafeToNotify; 71 /** 72 * Need to keep this around to look up pager title strings. 73 */ 74 private Resources mResources; 75 /** 76 * This isn't great to create a circular dependency, but our usage of {@link #getPageTitle(int)} 77 * requires knowing which page is the currently visible to dynamically name offscreen pages 78 * "newer" and "older". And {@link #setPrimaryItem(ViewGroup, int, Object)} does not work well 79 * because it isn't updated as often as {@link ViewPager#getCurrentItem()} is. 80 * <p> 81 * We must be careful to null out this reference when the pager and adapter are decoupled to 82 * minimize dangling references. 83 */ 84 private ViewPager mPager; 85 86 private static final String LOG_TAG = LogTag.getLogTag(); 87 88 private static final String BUNDLE_DETACHED_MODE = 89 ConversationPagerAdapter.class.getName() + "-detachedmode"; 90 91 public ConversationPagerAdapter(Resources res, FragmentManager fm, Account account, 92 Folder folder, Conversation initialConversation) { 93 super(fm, false /* enableSavedStates */); 94 mResources = res; 95 mCommonFragmentArgs = ConversationViewFragment.makeBasicArgs(account, folder); 96 mInitialConversation = initialConversation; 97 mAccount = account; 98 mFolder = folder; 99 } 100 101 public boolean matches(Account account, Folder folder) { 102 return mAccount != null && mFolder != null && mAccount.matches(account) 103 && mFolder.equals(folder); 104 } 105 106 public void setSingletonMode(boolean enabled) { 107 if (mSingletonMode != enabled) { 108 mSingletonMode = enabled; 109 notifyDataSetChanged(); 110 } 111 } 112 113 public boolean isSingletonMode() { 114 return mSingletonMode; 115 } 116 117 public boolean isPagingDisabled() { 118 return mSingletonMode || mDetachedMode || getCursor() == null; 119 } 120 121 private Cursor getCursor() { 122 if (mController == null) { 123 // Happens when someone calls setActivityController(null) on us. This is done in 124 // ConversationPagerController.stopListening() to indicate that the Conversation View 125 // is going away *very* soon. 126 LogUtils.i(LOG_TAG, "Pager adapter has a null controller. If the conversation view" 127 + " is going away, this is fine. Otherwise, the state is inconsistent"); 128 return null; 129 } 130 131 return mController.getConversationListCursor(); 132 } 133 134 @Override 135 public Fragment getItem(int position) { 136 final Conversation c; 137 138 if (isPagingDisabled()) { 139 // cursor-less adapter is a size-1 cursor that points to mInitialConversation. 140 // sanity-check 141 if (position != 0) { 142 LogUtils.wtf(LOG_TAG, "pager cursor is null and position is non-zero: %d", 143 position); 144 } 145 c = getDefaultConversation(); 146 c.position = 0; 147 } else { 148 final Cursor cursor = getCursor(); 149 if (cursor == null) { 150 LogUtils.wtf(LOG_TAG, "unable to get ConversationCursor, pos=%d", position); 151 return null; 152 } 153 if (!cursor.moveToPosition(position)) { 154 LogUtils.wtf(LOG_TAG, "unable to seek to ConversationCursor pos=%d (%s)", position, 155 cursor); 156 return null; 157 } 158 // TODO: switch to something like MessageCursor or AttachmentCursor 159 // to re-use these models 160 c = new Conversation(cursor); 161 c.position = position; 162 } 163 final Fragment f = ConversationViewFragment.newInstance(mCommonFragmentArgs, c); 164 LogUtils.d(LOG_TAG, "IN PagerAdapter.getItem, frag=%s subj=%s", f, c.subject); 165 return f; 166 } 167 168 @Override 169 public int getCount() { 170 if (isPagingDisabled()) { 171 LogUtils.d(LOG_TAG, "IN CPA.getCount, returning 1 (effective singleton). cursor=%s", 172 getCursor()); 173 return 1; 174 } 175 final Cursor cursor = getCursor(); 176 if (cursor == null) { 177 return 0; 178 } 179 return cursor.getCount(); 180 } 181 182 @Override 183 public int getItemPosition(Object item) { 184 if (!(item instanceof ConversationViewFragment)) { 185 LogUtils.wtf(LOG_TAG, "getItemPosition received unexpected item: %s", item); 186 } 187 188 final ConversationViewFragment fragment = (ConversationViewFragment) item; 189 return getConversationPosition(fragment.getConversation()); 190 } 191 192 @Override 193 public void setPrimaryItem(ViewGroup container, int position, Object object) { 194 LogUtils.d(LOG_TAG, "IN PagerAdapter.setPrimaryItem, pos=%d, frag=%s", position, 195 object); 196 super.setPrimaryItem(container, position, object); 197 } 198 199 @Override 200 public CharSequence getPageTitle(int position) { 201 final String title; 202 final int currentPosition = mPager.getCurrentItem(); 203 204 if (isPagingDisabled()) { 205 title = null; 206 } else if (position == currentPosition) { 207 int total = getCount(); 208 if (mController != null) { 209 final Folder f = mController.getFolder(); 210 if (f != null && f.totalCount > total) { 211 total = f.totalCount; 212 } 213 } 214 title = mResources.getString(R.string.conversation_count, position + 1, total); 215 } else { 216 title = mResources.getString(position < currentPosition ? 217 R.string.conversation_newer : R.string.conversation_older); 218 } 219 return title; 220 } 221 222 @Override 223 public Parcelable saveState() { 224 LogUtils.d(LOG_TAG, "IN PagerAdapter.saveState. this=%s", this); 225 Bundle state = (Bundle) super.saveState(); // superclass uses a Bundle 226 if (state == null) { 227 state = new Bundle(); 228 } 229 state.putBoolean(BUNDLE_DETACHED_MODE, mDetachedMode); 230 return state; 231 } 232 233 @Override 234 public void restoreState(Parcelable state, ClassLoader loader) { 235 LogUtils.d(LOG_TAG, "IN PagerAdapter.restoreState. this=%s", this); 236 super.restoreState(state, loader); 237 if (state != null) { 238 Bundle b = (Bundle) state; 239 b.setClassLoader(loader); 240 mDetachedMode = b.getBoolean(BUNDLE_DETACHED_MODE); 241 } 242 } 243 244 @Override 245 public void startUpdate(ViewGroup container) { 246 mSafeToNotify = false; 247 super.startUpdate(container); 248 } 249 250 @Override 251 public void finishUpdate(ViewGroup container) { 252 super.finishUpdate(container); 253 mSafeToNotify = true; 254 } 255 256 @Override 257 public void notifyDataSetChanged() { 258 if (!mSafeToNotify) { 259 LogUtils.d(LOG_TAG, "IN PagerAdapter.notifyDataSetChanged, ignoring unsafe update"); 260 return; 261 } 262 263 // when the currently visible item disappears from the dataset: 264 // if the new version of the currently visible item has zero messages: 265 // notify the list controller so it can handle this 'current conversation gone' case 266 // (by backing out of conversation mode) 267 // else 268 // 'detach' the conversation view from the cursor, keeping the current item as-is but 269 // disabling swipe (effectively the same as singleton mode) 270 if (mController != null) { 271 final Conversation currConversation = mController.getCurrentConversation(); 272 final int pos = getConversationPosition(currConversation); 273 if (pos == POSITION_NONE) { 274 // enable detached mode and do no more here. the fragment itself will figure out 275 // if the conversation is empty (using message list cursor) and back out if needed. 276 mDetachedMode = true; 277 LogUtils.i(LOG_TAG, "CPA: current conv is gone, reverting to detached mode. c=%s", 278 currConversation.uri); 279 } 280 } 281 282 super.notifyDataSetChanged(); 283 284 // notify unaffected fragment items of the change, so they can re-render 285 // (the change may have been to the labels for a single conversation, for example) 286 } 287 288 @Override 289 public void setItemVisible(Fragment item, boolean visible) { 290 super.setItemVisible(item, visible); 291 final ConversationViewFragment fragment = (ConversationViewFragment) item; 292 fragment.setExtraUserVisibleHint(visible); 293 294 if (visible && mController != null) { 295 final Conversation c = fragment.getConversation(); 296 LogUtils.d(LOG_TAG, "pager adapter setting current conv: %s (%s)", c.subject, item); 297 mController.setCurrentConversation(c); 298 } 299 } 300 301 private Conversation getDefaultConversation() { 302 Conversation c = (mController != null) ? mController.getCurrentConversation() : null; 303 if (c == null) { 304 c = mInitialConversation; 305 } 306 return c; 307 } 308 309 public int getConversationPosition(Conversation conv) { 310 if (isPagingDisabled()) { 311 if (getCursor() == null) { 312 return POSITION_NONE; 313 } 314 315 if (conv != getDefaultConversation()) { 316 LogUtils.d(LOG_TAG, "unable to find conversation in singleton mode. c=%s", 317 conv); 318 return POSITION_NONE; 319 } 320 return 0; 321 } 322 323 final Cursor cursor = getCursor(); 324 if (cursor == null) { 325 return POSITION_NONE; 326 } 327 328 final boolean networkWasEnabled = Utils.disableConversationCursorNetworkAccess(cursor); 329 330 int result = POSITION_NONE; 331 int pos = -1; 332 while (cursor.moveToPosition(++pos)) { 333 final long id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 334 if (conv.id == id) { 335 LogUtils.d(LOG_TAG, "pager adapter found repositioned convo '%s' at pos=%d", 336 conv.subject, pos); 337 result = pos; 338 break; 339 } 340 } 341 342 if (networkWasEnabled) { 343 Utils.enableConversationCursorNetworkAccess(cursor); 344 } 345 346 return result; 347 } 348 349 public void setPager(ViewPager pager) { 350 mPager = pager; 351 } 352 353 public void setActivityController(ActivityController controller) { 354 if (mController != null) { 355 mController.unregisterConversationListObserver(mListObserver); 356 mController.unregisterFolderObserver(mFolderObserver); 357 } 358 mController = controller; 359 if (mController != null) { 360 mController.registerConversationListObserver(mListObserver); 361 mController.registerFolderObserver(mFolderObserver); 362 363 notifyDataSetChanged(); 364 } else { 365 // We're being torn down; do not notify. 366 // Let the pager controller manage pager lifecycle. 367 } 368 } 369 370 // update the pager title strip as the Folder's conversation count changes 371 private class FolderObserver extends DataSetObserver { 372 @Override 373 public void onChanged() { 374 notifyDataSetChanged(); 375 } 376 } 377 378 // update the pager dataset as the Controller's cursor changes 379 private class ListObserver extends DataSetObserver { 380 @Override 381 public void onChanged() { 382 notifyDataSetChanged(); 383 } 384 @Override 385 public void onInvalidated() { 386 } 387 } 388 389} 390