conversation paging
This CL takes the approach of using a permanent ViewPager that
pages through conversation fragments. Its adapter is only
set and populated when the conversation view is shown. In all
other cases, it is an inert empty layer.
One risk with this approach is that it breaks with the typical
fragment transitions that all other content panes use.
On the other hand: conversation fragments are full-on fragments
and benefit from loader separation, and the FragmentManager
takes care of save/restore of state.
Change-Id: Ic17d1ae3f35a0cb1119967f2d34433ad27fa307c
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index ce0840e..2c96927 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -32,6 +32,7 @@
import com.android.mail.R;
import com.android.mail.browse.ScrollNotifier.ScrollListener;
+import com.android.mail.ui.ConversationViewFragment;
import com.android.mail.utils.DequeMap;
import com.android.mail.utils.LogUtils;
@@ -58,7 +59,7 @@
*/
public class ConversationContainer extends ViewGroup implements ScrollListener {
- private static final String TAG = new LogUtils().getLogTag();
+ private static final String TAG = ConversationViewFragment.LAYOUT_TAG;
private ConversationViewAdapter mOverlayAdapter;
private int[] mOverlayBottoms;
@@ -230,11 +231,18 @@
mTouchInitialized = true;
}
+ // no interception when WebView handles the first DOWN
+ if (mWebView.isHandlingTouch()) {
+ return false;
+ }
+
boolean intercept = false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
+ LogUtils.d(TAG, "Container is intercepting non-primary touch!");
intercept = true;
mMissedPointerDown = true;
+ requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_DOWN:
@@ -253,8 +261,8 @@
break;
}
- LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
- ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
+// LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
+// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
return intercept;
}
@@ -262,7 +270,7 @@
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
- if (action == MotionEvent.ACTION_UP) {
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mTouchIsDown = false;
} else if (!mTouchIsDown &&
(action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_POINTER_DOWN)) {
@@ -278,8 +286,8 @@
final boolean webViewResult = mWebView.onTouchEvent(ev);
- LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
- ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
+// LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
+// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
return webViewResult;
}
@@ -445,8 +453,11 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- LogUtils.i(TAG, "*** IN header container onMeasure spec for w/h=%d/%d", widthMeasureSpec,
- heightMeasureSpec);
+ if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
+ LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%s/%s",
+ MeasureSpec.toString(widthMeasureSpec),
+ MeasureSpec.toString(heightMeasureSpec));
+ }
if (mWebView.getVisibility() != GONE) {
measureChild(mWebView, widthMeasureSpec, heightMeasureSpec);
@@ -459,7 +470,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- LogUtils.i(TAG, "*** IN header container onLayout");
+ LogUtils.d(TAG, "*** IN header container onLayout");
mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
positionOverlays(0, mOffsetY);
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
index bc87147..e318c0a 100644
--- a/src/com/android/mail/browse/ConversationOverlayItem.java
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -24,13 +24,14 @@
import android.widget.Adapter;
import android.widget.CursorAdapter;
+import com.android.mail.ui.ConversationViewFragment;
import com.android.mail.utils.LogUtils;
public abstract class ConversationOverlayItem {
private int mHeight; // in px
private boolean mNeedsMeasure;
- public static final String LOG_TAG = new LogUtils().getLogTag();
+ public static final String LOG_TAG = ConversationViewFragment.LAYOUT_TAG;
/**
* @see Adapter#getItemViewType(int)
diff --git a/src/com/android/mail/browse/ConversationPager.java b/src/com/android/mail/browse/ConversationPager.java
new file mode 100644
index 0000000..a877e32
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationPager.java
@@ -0,0 +1,36 @@
+/*
+ * 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.content.Context;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+
+public class ConversationPager extends ViewPager {
+
+ public ConversationPager(Context context) {
+ this(context, null);
+ }
+
+ public ConversationPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ // this space intentionally blank (reserved for various debugging hooks)
+
+}
diff --git a/src/com/android/mail/browse/ConversationPagerAdapter.java b/src/com/android/mail/browse/ConversationPagerAdapter.java
new file mode 100644
index 0000000..2aaf37b
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationPagerAdapter.java
@@ -0,0 +1,244 @@
+/*
+ * 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.Fragment;
+import android.app.FragmentManager;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.ViewGroup;
+
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.ConversationListCallbacks;
+import com.android.mail.ui.ConversationViewFragment;
+import com.android.mail.utils.FragmentStatePagerAdapter2;
+import com.android.mail.utils.LogUtils;
+
+public class ConversationPagerAdapter extends FragmentStatePagerAdapter2 {
+
+ private final DataSetObserver mListObserver = new ListObserver();
+ private ConversationListCallbacks mListProxy;
+ private Cursor mCursor;
+ private final Bundle mCommonFragmentArgs;
+ private final Conversation mInitialConversation;
+ /**
+ * In singleton mode, this adapter ignores the cursor contents and size, and acts as if the
+ * data set size is exactly size=1, with {@link #mInitialConversation} at position 0.
+ */
+ private boolean mSingletonMode = true;
+ /**
+ * Adapter methods may trigger a data set change notification in the middle of a ViewPager
+ * update, but they are not safe to handle, so we have to ignore them. This will not ignore
+ * pager-external updates; it's impossible to be notified of an external change during
+ * an update.
+ *
+ * TODO: Queue up changes like this, if there ever are any that actually modify the data set.
+ * Right now there are none. Such a change would have to be of the form: instantiation or
+ * setPrimary somehow adds or removes items from the conversation cursor. Crazy!
+ */
+ private boolean mSafeToNotify;
+
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ public ConversationPagerAdapter(FragmentManager fm, Account account, Folder folder,
+ Conversation initialConversation) {
+ super(fm, false /* enableSavedStates */);
+ mCommonFragmentArgs = ConversationViewFragment.makeBasicArgs(account, folder);
+ mInitialConversation = initialConversation;
+ }
+
+ public void setSingletonMode(boolean enabled) {
+ if (mSingletonMode != enabled) {
+ mSingletonMode = enabled;
+ notifyDataSetChanged();
+ }
+ }
+
+ public boolean isSingletonMode() {
+ return mSingletonMode || mCursor == null;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ final Conversation c;
+
+ if (isSingletonMode()) {
+ // cursor-less adapter is a size-1 cursor that points to mInitialConversation.
+ // sanity-check
+ if (position != 0) {
+ LogUtils.wtf(LOG_TAG, "pager cursor is null and position is non-zero: %d",
+ position);
+ }
+ c = mInitialConversation;
+ } else {
+ if (!mCursor.moveToPosition(position)) {
+ LogUtils.wtf(LOG_TAG, "unable to seek to ConversationCursor pos=%d (%s)", position,
+ mCursor);
+ return null;
+ }
+ // TODO: switch to something like MessageCursor or AttachmentCursor
+ // to re-use these models
+ c = new Conversation(mCursor);
+ }
+
+ final Fragment f = ConversationViewFragment.newInstance(mCommonFragmentArgs, c);
+ LogUtils.d(LOG_TAG, "IN PagerAdapter.getItem, frag=%s subj=%s", f, c.subject);
+ return f;
+ }
+
+ @Override
+ public int getCount() {
+ return (isSingletonMode()) ? 1 : mCursor.getCount();
+ }
+
+ @Override
+ public int getItemPosition(Object item) {
+ if (!(item instanceof ConversationViewFragment)) {
+ LogUtils.wtf(LOG_TAG, "getItemPosition received unexpected item: %s", item);
+ }
+
+ final ConversationViewFragment fragment = (ConversationViewFragment) item;
+ return getConversationPosition(fragment.getConversation());
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ LogUtils.d(LOG_TAG, "IN PagerAdapter.setPrimaryItem, pos=%d, frag=%s", position,
+ object);
+ super.setPrimaryItem(container, position, object);
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+
+ // TODO: implement this to show "1 of 123" or whatever
+ // maybe when the position is not the pager's current position, this could
+ // return "newer" or "older"?
+
+ return null;
+ }
+
+ @Override
+ public Parcelable saveState() {
+ LogUtils.d(LOG_TAG, "IN PagerAdapter.saveState");
+ return super.saveState();
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ LogUtils.d(LOG_TAG, "IN PagerAdapter.restoreState");
+ super.restoreState(state, loader);
+ }
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ mSafeToNotify = false;
+ super.startUpdate(container);
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ super.finishUpdate(container);
+ mSafeToNotify = true;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ if (!mSafeToNotify) {
+ LogUtils.d(LOG_TAG, "IN PagerAdapter.notifyDataSetChanged, ignoring unsafe update");
+ return;
+ }
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void setItemVisible(Fragment item, boolean visible) {
+ super.setItemVisible(item, visible);
+ final ConversationViewFragment fragment = (ConversationViewFragment) item;
+ fragment.setExtraUserVisibleHint(visible);
+
+ if (visible && mListProxy != null) {
+ final Conversation c = fragment.getConversation();
+ LogUtils.d(LOG_TAG, "pager adapter setting current conv: %s (%s)", c.subject, item);
+ mListProxy.setCurrentConversation(c);
+ }
+ }
+
+ public void swapCursor(Cursor listCursor) {
+ mCursor = listCursor;
+ notifyDataSetChanged();
+ }
+
+ public int getConversationPosition(Conversation conv) {
+ if (isSingletonMode()) {
+ if (conv != mInitialConversation) {
+ LogUtils.wtf(LOG_TAG, "unable to find conversation with null pager cursor. c=%s",
+ conv);
+ return POSITION_NONE;
+ }
+ return 0;
+ }
+
+ int pos = -1;
+ while (mCursor.moveToPosition(++pos)) {
+ final long id = mCursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
+ if (conv.id == id) {
+ LogUtils.d(LOG_TAG, "pager adapter found repositioned convo '%s' at pos=%d",
+ conv.subject, pos);
+ return pos;
+ }
+ }
+ // This should just about never happen. Clients should never pass a missing conversation,
+ // or else we will run through the entire (very large) cursor for nothing, possibly even
+ // incurring side effects like network fetches.
+ LogUtils.wtf(LOG_TAG, new Error(),
+ "PagerAdapter iterated over entire conversation cursor with no match");
+ return POSITION_NONE;
+ }
+
+ public void setListProxy(ConversationListCallbacks listProxy) {
+ if (mListProxy != null) {
+ mListProxy.unregisterConversationListObserver(mListObserver);
+ }
+ mListProxy = listProxy;
+ if (mListProxy != null) {
+ mListProxy.registerConversationListObserver(mListObserver);
+
+ swapCursor(mListProxy.getConversationListCursor());
+ } else {
+ mCursor = null;
+ }
+ }
+
+ // update the pager dataset as the Controller's cursor changes
+ private class ListObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ swapCursor(mListProxy.getConversationListCursor());
+ }
+ @Override
+ public void onInvalidated() {
+ }
+ }
+
+}
diff --git a/src/com/android/mail/browse/ConversationPagerController.java b/src/com/android/mail/browse/ConversationPagerController.java
new file mode 100644
index 0000000..afc52ec
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationPagerController.java
@@ -0,0 +1,144 @@
+/*
+ * 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.support.v4.view.ViewPager;
+
+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.ConversationListCallbacks;
+import com.android.mail.ui.RestrictedActivity;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * 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 ConversationPager mPager;
+ private ConversationPagerAdapter mPagerAdapter;
+ private FragmentManager mFragmentManager;
+ private ConversationListCallbacks mListProxy;
+ private boolean mShown;
+
+ private static final String LOG_TAG = new LogUtils().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.
+ * <p>
+ * However, this is disabled right now because ViewPager seems to have a bug when you enable
+ * this adapter data set change when the initial current item was item #1.
+ */
+ private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = false;
+
+ public ConversationPagerController(RestrictedActivity activity,
+ ConversationListCallbacks listProxy) {
+ mFragmentManager = activity.getFragmentManager();
+ mPager = (ConversationPager) activity.findViewById(R.id.conversation_pane);
+ mListProxy = listProxy;
+ }
+
+ public void show(Account account, Folder folder, Conversation initialConversation) {
+ if (mShown) {
+ LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
+ cleanup();
+
+ // TODO: can optimize this case to shuffle the existing adapter to jump to a new
+ // position if the account+folder combo are the same, but the conversation is different
+ }
+
+ mPagerAdapter = new ConversationPagerAdapter(mFragmentManager, account, folder,
+ initialConversation);
+ mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
+ mPagerAdapter.setListProxy(mListProxy);
+ LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
+
+ LogUtils.d(LOG_TAG, "init pager adapter, count=%d initial=%s", mPagerAdapter.getCount(),
+ initialConversation.subject);
+ 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);
+ LogUtils.w(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
+ mPager.setCurrentItem(initialPos);
+ }
+
+ mShown = true;
+ }
+
+ public void hide() {
+ if (!mShown) {
+ LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
+ return;
+ }
+ mShown = false;
+
+ LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
+ mPager.setAdapter(null);
+ cleanup();
+ }
+
+ 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.setListProxy(null);
+ mPagerAdapter = null;
+ }
+ }
+
+ public void onConversationSeen(Conversation conv) {
+ // take the adapter out of singleton mode to begin loading the
+ // other non-visible conversations
+ if (mPagerAdapter.isSingletonMode()) {
+ LogUtils.d(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
+ " switching to cursor mode to load other conversations");
+ mPagerAdapter.setSingletonMode(false);
+ }
+ }
+}
diff --git a/src/com/android/mail/browse/ConversationWebView.java b/src/com/android/mail/browse/ConversationWebView.java
index 2d80138..2f89c1a 100644
--- a/src/com/android/mail/browse/ConversationWebView.java
+++ b/src/com/android/mail/browse/ConversationWebView.java
@@ -22,6 +22,8 @@
import android.view.MotionEvent;
import android.webkit.WebView;
+import com.android.mail.utils.LogUtils;
+
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -30,6 +32,14 @@
private final Set<ScrollListener> mScrollListeners =
new CopyOnWriteArraySet<ScrollListener>();
+ /**
+ * True when WebView is handling a touch-- in between POINTER_DOWN and
+ * POINTER_UP/POINTER_CANCEL.
+ */
+ private boolean mHandlingTouch;
+
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
public ConversationWebView(Context c) {
this(c, null);
}
@@ -59,14 +69,27 @@
@Override
public boolean onTouchEvent(MotionEvent ev) {
- boolean result = super.onTouchEvent(ev);
+ final int action = ev.getActionMasked();
- if (result) {
- // Events handled by the WebView should not be monkeyed with by any overlay interceptor
- requestDisallowInterceptTouchEvent(true);
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mHandlingTouch = true;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
+ requestDisallowInterceptTouchEvent(true);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mHandlingTouch = false;
+ break;
}
- return result;
+ return super.onTouchEvent(ev);
+ }
+
+ public boolean isHandlingTouch() {
+ return mHandlingTouch;
}
}
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 8a6f79e..01c1937 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -34,6 +34,8 @@
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -52,6 +54,7 @@
import com.android.mail.ConversationListContext;
import com.android.mail.R;
import com.android.mail.browse.ConversationCursor;
+import com.android.mail.browse.ConversationPagerController;
import com.android.mail.browse.ConversationCursor.ConversationListener;
import com.android.mail.browse.SelectedConversationsActionMenu;
import com.android.mail.compose.ComposeActivity;
@@ -112,8 +115,6 @@
protected static final String TAG_WAIT = "wait-fragment";
/** Tag used when loading a conversation list fragment. */
protected static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
- /** Tag used when loading a conversation fragment. */
- protected static final String TAG_CONVERSATION = "tag-conversation";
/** Tag used when loading a folder list fragment. */
protected static final String TAG_FOLDER_LIST = "tag-folder-list";
@@ -149,6 +150,22 @@
private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
protected Settings mCachedSettings;
protected ConversationCursor mConversationListCursor;
+ private final DataSetObservable mConversationListObservable = new DataSetObservable() {
+ @Override
+ public void registerObserver(DataSetObserver observer) {
+ final int count = mObservers.size();
+ super.registerObserver(observer);
+ LogUtils.d(LOG_TAG, "IN AAC.registerListObserver: %s before=%d after=%d", observer,
+ count, mObservers.size());
+ }
+ @Override
+ public void unregisterObserver(DataSetObserver observer) {
+ final int count = mObservers.size();
+ super.unregisterObserver(observer);
+ LogUtils.d(LOG_TAG, "IN AAC.unregisterListObserver: %s before=%d after=%d", observer,
+ count, mObservers.size());
+ }
+ };
protected boolean mConversationListenerAdded = false;
private boolean mIsConversationListScrolling = false;
@@ -175,6 +192,7 @@
*/
SelectedConversationsActionMenu mCabActionMenu;
protected UndoBarView mUndoBarView;
+ protected ConversationPagerController mPagerController;
// this is split out from the general loader dispatcher because its loader doesn't return a
// basic Cursor
@@ -275,19 +293,6 @@
}
/**
- * Returns the conversation view fragment attached with this activity. If no such fragment
- * is attached, this method returns null.
- * @return
- */
- protected ConversationViewFragment getConversationViewFragment() {
- final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION);
- if (isValidFragment(fragment)) {
- return (ConversationViewFragment) fragment;
- }
- return null;
- }
-
- /**
* Returns the folder list fragment attached with this activity. If no such fragment is attached
* this method returns null.
* @return
@@ -547,13 +552,32 @@
assert (mActionBarView != null);
mViewMode.addListener(mActionBarView);
- restoreState(savedState);
- mUndoBarView = (UndoBarView) mActivity.findViewById(R.id.undo_view);
- return true;
- }
+ mPagerController = new ConversationPagerController(mActivity, this);
- @Override
- public void onRestoreInstanceState(Bundle savedInstanceState) {
+ mUndoBarView = (UndoBarView) mActivity.findViewById(R.id.undo_view);
+
+ final Intent intent = mActivity.getIntent();
+ // immediately handle a clean launch with intent, and any state restoration
+ // that does not rely on restored fragments or loader data
+ // any state restoration that relies on those can be done later in
+ // onRestoreInstanceState, once fragments are up and loader data is re-delivered
+ if (savedState != null) {
+ if (savedState.containsKey(SAVED_ACCOUNT)) {
+ setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
+ mActivity.invalidateOptionsMenu();
+ }
+ if (savedState.containsKey(SAVED_FOLDER)) {
+ // Open the folder.
+ onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER));
+ }
+ } else if (intent != null) {
+ handleIntent(intent);
+ }
+
+ // Create the accounts loader; this loads the account switch spinner.
+ mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
+
+ return true;
}
@Override
@@ -804,6 +828,12 @@
// TODO(viki): Auto-generated method stub
}
+ @Override
+ public void onDestroy() {
+ // unregister the ViewPager's observer on the conversation cursor
+ mPagerController.onDestroy();
+ }
+
/**
* {@inheritDoc} Subclasses must override this to listen to mode changes
* from the ViewMode. Subclasses <b>must</b> call the parent's
@@ -816,11 +846,6 @@
// controllers do
// this themselves?
- // In conversation list mode, clean up the conversation.
- if (newMode == ViewMode.CONVERSATION_LIST) {
- // Clean up the conversation here.
- }
-
// We don't want to invalidate the options menu when switching to
// conversation
// mode, as it will happen when the conversation finishes loading.
@@ -854,90 +879,13 @@
*
* @param savedState
*/
- protected void restoreState(Bundle savedState) {
- final Intent intent = mActivity.getIntent();
- boolean handled = false;
- if (savedState != null) {
- if (savedState.containsKey(SAVED_ACCOUNT)) {
- setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
- mActivity.invalidateOptionsMenu();
- }
- if (savedState.containsKey(SAVED_FOLDER)) {
- // Open the folder.
- onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER));
- handled = true;
- }
- if (savedState.containsKey(SAVED_CONVERSATION)) {
- // Open the conversation.
- setCurrentConversation((Conversation) savedState.getParcelable(SAVED_CONVERSATION));
- showConversation(mCurrentConversation);
- handled = true;
- }
- } else if (intent != null) {
- if (Intent.ACTION_VIEW.equals(intent.getAction())) {
- if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
- setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
- } else if (intent.hasExtra(Utils.EXTRA_ACCOUNT_STRING)) {
- setAccount(Account.newinstance(intent
- .getStringExtra(Utils.EXTRA_ACCOUNT_STRING)));
- }
- if (mAccount != null) {
- mActivity.invalidateOptionsMenu();
- }
-
- Folder folder = null;
- if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
- // Open the folder.
- LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s",
- intent.getParcelableExtra(Utils.EXTRA_FOLDER));
- folder = (Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER);
-
- } else if (intent.hasExtra(Utils.EXTRA_FOLDER_STRING)) {
- // Open the folder.
- folder = new Folder(intent.getStringExtra(Utils.EXTRA_FOLDER_STRING));
- }
- if (folder != null) {
- onFolderChanged(folder);
- handled = true;
- }
-
- if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
- // Open the conversation.
- LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
- intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
- setCurrentConversation((Conversation) intent
- .getParcelableExtra(Utils.EXTRA_CONVERSATION));
- showConversation(mCurrentConversation);
- handled = true;
- }
-
- if (!handled) {
- // Nothing was saved; just load the account inbox.
- loadAccountInbox();
- }
- } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
- if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
- // Save this search query for future suggestions.
- final String query = intent.getStringExtra(SearchManager.QUERY);
- final String authority = mContext.getString(R.string.suggestions_authority);
- SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
- mContext, authority, SuggestionsProvider.MODE);
- suggestions.saveRecentQuery(query, null);
-
- mViewMode.enterSearchResultsListMode();
- setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
- mActivity.invalidateOptionsMenu();
- restartOptionalLoader(LOADER_RECENT_FOLDERS);
- mRecentFolderList.setCurrentAccount(mAccount);
- fetchSearchFolder(intent);
- } else {
- LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
- mActivity.finish();
- }
- }
- if (mAccount != null) {
- restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
- }
+ @Override
+ public void onRestoreInstanceState(Bundle savedState) {
+ LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
+ if (savedState.containsKey(SAVED_CONVERSATION)) {
+ // Open the conversation.
+ setCurrentConversation((Conversation) savedState.getParcelable(SAVED_CONVERSATION));
+ showConversation(mCurrentConversation);
}
/**
@@ -947,8 +895,74 @@
* @param savedState
*/
restoreSelectedConversations(savedState);
- // Create the accounts loader; this loads the account switch spinner.
- mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
+ }
+
+ private void handleIntent(Intent intent) {
+ boolean handled = false;
+ if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
+ setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
+ } else if (intent.hasExtra(Utils.EXTRA_ACCOUNT_STRING)) {
+ setAccount(Account.newinstance(intent
+ .getStringExtra(Utils.EXTRA_ACCOUNT_STRING)));
+ }
+ if (mAccount != null) {
+ mActivity.invalidateOptionsMenu();
+ }
+
+ Folder folder = null;
+ if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
+ // Open the folder.
+ LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s",
+ intent.getParcelableExtra(Utils.EXTRA_FOLDER));
+ folder = (Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER);
+
+ } else if (intent.hasExtra(Utils.EXTRA_FOLDER_STRING)) {
+ // Open the folder.
+ folder = new Folder(intent.getStringExtra(Utils.EXTRA_FOLDER_STRING));
+ }
+ if (folder != null) {
+ onFolderChanged(folder);
+ handled = true;
+ }
+
+ if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
+ // Open the conversation.
+ LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
+ intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
+ setCurrentConversation((Conversation) intent
+ .getParcelableExtra(Utils.EXTRA_CONVERSATION));
+ showConversation(mCurrentConversation);
+ handled = true;
+ }
+
+ if (!handled) {
+ // Nothing was saved; just load the account inbox.
+ loadAccountInbox();
+ }
+ } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
+ // Save this search query for future suggestions.
+ final String query = intent.getStringExtra(SearchManager.QUERY);
+ final String authority = mContext.getString(R.string.suggestions_authority);
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
+ mContext, authority, SuggestionsProvider.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ mViewMode.enterSearchResultsListMode();
+ setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
+ mActivity.invalidateOptionsMenu();
+ restartOptionalLoader(LOADER_RECENT_FOLDERS);
+ mRecentFolderList.setCurrentAccount(mAccount);
+ fetchSearchFolder(intent);
+ } else {
+ LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
+ mActivity.finish();
+ }
+ }
+ if (mAccount != null) {
+ restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
+ }
}
/**
@@ -966,6 +980,8 @@
mSelectedSet.clear();
return;
}
+
+ // putAll will take care of calling our registered onSetPopulated method
mSelectedSet.putAll(selectedSet);
}
@@ -1045,7 +1061,8 @@
* perform common actions associated with changing the current conversation.
* @param conversation
*/
- protected void setCurrentConversation(Conversation conversation) {
+ @Override
+ public void setCurrentConversation(Conversation conversation) {
mCurrentConversation = conversation;
mTracker.initialize(mCurrentConversation);
}
@@ -1131,6 +1148,16 @@
lm.initLoader(id, Bundle.EMPTY, this);
}
+ @Override
+ public void registerConversationListObserver(DataSetObserver observer) {
+ mConversationListObservable.registerObserver(observer);
+ }
+
+ @Override
+ public void unregisterConversationListObserver(DataSetObserver observer) {
+ mConversationListObservable.unregisterObserver(observer);
+ }
+
private boolean accountsUpdated(Cursor accountCursor) {
// Check to see if the current account hasn't been set, or the account cursor is empty
if (mAccount == null || !accountCursor.moveToFirst()) {
@@ -1506,6 +1533,8 @@
@Override
public void onDataSetChanged() {
refreshAdapter();
+
+ mConversationListObservable.notifyChanged();
}
private void refreshAdapter() {
@@ -1712,6 +1741,11 @@
}
}
+ @Override
+ public void onConversationSeen(Conversation conv) {
+ mPagerController.onConversationSeen(conv);
+ }
+
private class ConversationListLoaderCallbacks implements
LoaderManager.LoaderCallbacks<ConversationCursor> {
@@ -1724,6 +1758,8 @@
@Override
public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
+ LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
+ data, loader);
mConversationListCursor = data;
// Call the method that updates things when values in the cursor change
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 4b4c497..5d2e3c6 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -147,6 +147,11 @@
void onPause();
/**
+ * @see android.app.Activity#onDestroy
+ */
+ void onDestroy();
+
+ /**
* @see android.app.Activity#onPrepareDialog
* @param id
* @param dialog
@@ -280,4 +285,10 @@
void onUndoCancel();
+ /**
+ * Coordinates actions that might occur in response to a conversation that has finished loading
+ * and is now user-visible.
+ */
+ void onConversationSeen(Conversation conv);
+
}
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index 7018282..3950a98 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -17,6 +17,7 @@
package com.android.mail.ui;
+import com.android.mail.providers.Conversation;
import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.ui.FolderListFragment;
@@ -75,4 +76,6 @@
FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
DragListener getDragListener();
+
+ void onConversationSeen(Conversation conv);
}
diff --git a/src/com/android/mail/ui/ConversationListCallbacks.java b/src/com/android/mail/ui/ConversationListCallbacks.java
index c493a1e..3d6a85f 100644
--- a/src/com/android/mail/ui/ConversationListCallbacks.java
+++ b/src/com/android/mail/ui/ConversationListCallbacks.java
@@ -17,6 +17,8 @@
package com.android.mail.ui;
+import android.database.DataSetObserver;
+
import com.android.mail.browse.ConversationCursor;
import com.android.mail.providers.Conversation;
@@ -32,4 +34,9 @@
void onConversationSelected(Conversation conversation);
ConversationCursor getConversationListCursor();
+
+ void setCurrentConversation(Conversation c);
+
+ void registerConversationListObserver(DataSetObserver observer);
+ void unregisterConversationListObserver(DataSetObserver observer);
}
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 729821d..0d03431 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -402,12 +402,12 @@
* @param position
*/
protected void viewConversation(int position) {
+ getListView().setItemChecked(position, true);
ConversationCursor conversationListCursor = getConversationListCursor();
conversationListCursor.moveToPosition(position);
Conversation conv = new Conversation(conversationListCursor);
conv.position = position;
mCallbacks.onConversationSelected(conv);
- getListView().setItemChecked(position, true);
}
private ConversationCursor getConversationListCursor() {
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 914cf9c..75f0ee8 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -84,6 +84,7 @@
SuperCollapsedBlock.OnClickListener {
private static final String LOG_TAG = new LogUtils().getLogTag();
+ public static final String LAYOUT_TAG = "ConvLayout";
private static final int MESSAGE_LOADER_ID = 0;
@@ -118,6 +119,9 @@
private float mDensity;
+ /**
+ * Folder is used to help determine valid menu actions for this conversation.
+ */
private Folder mFolder;
private final Map<String, Address> mAddressCache = Maps.newHashMap();
@@ -130,8 +134,14 @@
*/
private String mTempBodiesHtml;
+ private boolean mUserVisible;
+
+ private int mMaxAutoLoadMessages;
+
+ private boolean mDeferredConversationLoad;
+
private static final String ARG_ACCOUNT = "account";
- private static final String ARG_CONVERSATION = "conversation";
+ public static final String ARG_CONVERSATION = "conversation";
private static final String ARG_FOLDER = "folder";
/**
@@ -143,7 +153,7 @@
/**
* Creates a new instance of {@link ConversationViewFragment}, initialized
- * to display conversation.
+ * to display a conversation.
*/
public static ConversationViewFragment newInstance(Account account,
Conversation conversation, Folder folder) {
@@ -156,8 +166,31 @@
return f;
}
+ /**
+ * Creates a new instance of {@link ConversationViewFragment}, initialized
+ * to display a conversation with other parameters inherited/copied from an existing bundle,
+ * typically one created using {@link #makeBasicArgs}.
+ */
+ public static ConversationViewFragment newInstance(Bundle existingArgs,
+ Conversation conversation) {
+ ConversationViewFragment f = new ConversationViewFragment();
+ Bundle args = new Bundle(existingArgs);
+ args.putParcelable(ARG_CONVERSATION, conversation);
+ f.setArguments(args);
+ return f;
+ }
+
+ public static Bundle makeBasicArgs(Account account, Folder folder) {
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_ACCOUNT, account);
+ args.putParcelable(ARG_FOLDER, folder);
+ return args;
+ }
+
@Override
public void onActivityCreated(Bundle savedInstanceState) {
+ LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this,
+ mConversation.subject);
super.onActivityCreated(savedInstanceState);
// Strictly speaking, we get back an android.app.Activity from getActivity. However, the
// only activity creating a ConversationListContext is a MailActivity which is of type
@@ -183,13 +216,14 @@
mDensity = getResources().getDisplayMetrics().density;
- // Show conversation and start loading messages.
+ mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
+
showConversation();
}
@Override
public void onCreate(Bundle savedState) {
- LogUtils.v(LOG_TAG, "onCreate in FolderListFragment(this=%s)", this);
+ LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
super.onCreate(savedState);
Bundle args = getArguments();
@@ -205,7 +239,7 @@
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.conversation_view, null);
+ View rootView = inflater.inflate(R.layout.conversation_view, container, false);
mConversationContainer = (ConversationContainer) rootView
.findViewById(R.id.conversation_container);
mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
@@ -284,14 +318,54 @@
&& mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
&& !mConversation.muted);
}
+
+ /**
+ * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
+ * reliability on older platforms.
+ */
+ public void setExtraUserVisibleHint(boolean isVisibleToUser) {
+ LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
+
+ if (mUserVisible != isVisibleToUser) {
+ mUserVisible = isVisibleToUser;
+
+ if (isVisibleToUser && mViewsCreated) {
+
+ if (mCursor == null && mDeferredConversationLoad) {
+ // load
+ LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s",
+ mConversation.uri);
+ showConversation();
+ mDeferredConversationLoad = false;
+ } else {
+ onConversationSeen();
+ }
+
+ }
+ }
+ }
+
/**
* Handles a request to show a new conversation list, either from a search query or for viewing
* a folder. This will initiate a data load, and hence must be called on the UI thread.
*/
private void showConversation() {
+ if (!mUserVisible && mConversation.numMessages > mMaxAutoLoadMessages) {
+ LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s",
+ mConversation.uri);
+ mDeferredConversationLoad = true;
+ return;
+ }
+ LogUtils.v(LOG_TAG,
+ "Fragment is short or user-visible, immediately rendering conversation: %s",
+ mConversation.uri);
getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this);
}
+ public Conversation getConversation() {
+ return mConversation;
+ }
+
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MessageLoader(mContext, mConversation.messageListUri);
@@ -301,6 +375,12 @@
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
MessageCursor messageCursor = (MessageCursor) data;
+ // ignore truly duplicate results
+ // this can happen when restoring after rotation
+ if (mCursor == messageCursor) {
+ return;
+ }
+
// TODO: handle Gmail loading states (like LOADING and ERROR)
if (messageCursor.getCount() == 0) {
if (mCursor != null) {
@@ -312,6 +392,10 @@
return;
}
+ // TODO: if this is not user-visible, delay render until user-visible fragment is done.
+ // This is needed in addition to the showConversation() delay to speed up rotation and
+ // restoration.
+
renderConversation(messageCursor);
}
@@ -343,6 +427,9 @@
*/
private String renderMessageBodies(MessageCursor messageCursor) {
int pos = -1;
+
+ LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s subj=%s", this,
+ mConversation.subject);
boolean allowNetworkImages = false;
// TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
@@ -501,6 +588,19 @@
return (int) (heightPx / mDensity);
}
+ private void onConversationSeen() {
+ // mark as read upon open
+ if (!mConversation.read) {
+ mConversation.markRead(mContext, true /* read */);
+ mConversation.read = true;
+ }
+
+ ControllableActivity activity = (ControllableActivity) getActivity();
+ if (activity != null) {
+ activity.onConversationSeen(mConversation);
+ }
+ }
+
// BEGIN conversation header callbacks
@Override
public void onFoldersClicked() {
@@ -530,7 +630,7 @@
mConversationContainer.invalidateSpacerGeometry();
// update message HTML spacer height
- LogUtils.i(LOG_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx);
+ LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx);
final int heightDp = (int) (newSpacerHeightPx / mDensity);
mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
mTemplates.getMessageDomId(item.message), heightDp));
@@ -541,7 +641,7 @@
mConversationContainer.invalidateSpacerGeometry();
// show/hide the HTML message body and update the spacer height
- LogUtils.i(LOG_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(),
+ LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(),
newSpacerHeightPx);
final int heightDp = (int) (newSpacerHeightPx / mDensity);
mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
@@ -613,15 +713,16 @@
@Override
public void onPageFinished(WebView view, String url) {
+ LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s", url,
+ ConversationViewFragment.this);
+
super.onPageFinished(view, url);
// TODO: save off individual message unread state (here, or in onLoadFinished?) so
// 'mark unread' restores the original unread state for each individual message
- // mark as read upon open
- if (!mConversation.read) {
- mConversation.markRead(mContext, true /* read */);
- mConversation.read = true;
+ if (mUserVisible) {
+ onConversationSeen();
}
}
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index 2d93b71..e0423b3 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -35,6 +35,7 @@
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.providers.Settings;
import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
@@ -314,4 +315,9 @@
public void onUndoAvailable(UndoOperation undoOp) {
// Do nothing.
}
+
+ @Override
+ public void onConversationSeen(Conversation conv) {
+ // Do nothing.
+ }
}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index 8540a4e..fd64237 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -29,6 +29,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
+import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.Settings;
import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
@@ -220,6 +221,12 @@
}
@Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mController.onDestroy();
+ }
+
+ @Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mController.onWindowFocusChanged(hasFocus);
@@ -289,4 +296,9 @@
public void onUndoAvailable(UndoOperation undoOp) {
mController.onUndoAvailable(undoOp);
}
+
+ @Override
+ public void onConversationSeen(Conversation conv) {
+ mController.onConversationSeen(conv);
+ }
}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index df55ba1..e454884 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -18,6 +18,7 @@
package com.android.mail.ui;
import android.app.Fragment;
+import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.database.Cursor;
import android.net.Uri;
@@ -82,8 +83,8 @@
}
@Override
- protected void restoreState(Bundle inState) {
- super.restoreState(inState);
+ public void onRestoreInstanceState(Bundle inState) {
+ super.onRestoreInstanceState(inState);
// TODO(mindyp) handle saved state.
if (inState != null) {
mLastFolderListTransactionId = inState.getInt(FOLDER_LIST_TRANSACTION_KEY, INVALID_ID);
@@ -147,6 +148,13 @@
@Override
public void onViewModeChanged(int newMode) {
super.onViewModeChanged(newMode);
+
+ // When entering conversation list mode, hide and clean up any currently visible
+ // conversation.
+ // TODO: improve this transition
+ if (newMode == ViewMode.CONVERSATION_LIST) {
+ mPagerController.hide();
+ }
}
@Override
@@ -190,9 +198,26 @@
} else {
mViewMode.enterConversationMode();
}
- mLastConversationTransactionId = replaceFragment(
- ConversationViewFragment.newInstance(mAccount, conversation, mFolder),
- FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_CONVERSATION);
+
+ // Switching to conversation view is an incongruous transition: we are not replacing a
+ // fragment with another fragment as usual. Instead, reveal the heretofore inert
+ // conversation ViewPager and just remove the previously visible fragment
+ // (e.g. conversation list, or possibly label list?).
+ FragmentManager fm = mActivity.getFragmentManager();
+ Fragment f = fm.findFragmentById(R.id.content_pane);
+ if (f != null) {
+ FragmentTransaction ft = fm.beginTransaction();
+ ft.addToBackStack(null);
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
+ ft.remove(f);
+ ft.commitAllowingStateLoss();
+ }
+
+ // TODO: improve this transition
+ mPagerController.show(mAccount, mFolder, conversation);
+
+ resetActionBarIcon();
+
mConversationListVisible = false;
}
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 28cdd35..7a3ace9 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -42,11 +42,11 @@
import android.widget.FrameLayout;
/**
- * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
- * limited.
+ * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
+ * abounds.
*/
-// Called OnePaneActivityController in Gmail.
+// Called TwoPaneActivityController in Gmail.
public final class TwoPaneController extends AbstractActivityController {
private TwoPaneLayout mLayout;
private final ActionCompleteListener mDeleteListener = new TwoPaneDestructiveActionListener(
@@ -194,6 +194,15 @@
}
@Override
+ public void onConversationVisibilityChanged(boolean visible) {
+ super.onConversationVisibilityChanged(visible);
+
+ if (!visible) {
+ mPagerController.hide();
+ }
+ }
+
+ @Override
public void resetActionBarIcon() {
if (mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
mActionBarView.removeBackButton();
@@ -215,12 +224,8 @@
} else {
mViewMode.enterConversationMode();
}
- Fragment convFragment = ConversationViewFragment.newInstance(mAccount, conversation,
- mFolder);
- FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
- fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
- fragmentTransaction.replace(R.id.conversation_pane, convFragment, TAG_CONVERSATION);
- fragmentTransaction.commitAllowingStateLoss();
+
+ mPagerController.show(mAccount, mFolder, conversation);
}
@Override
@@ -487,7 +492,6 @@
}
break;
case ViewMode.CONVERSATION:
- final ConversationViewFragment convView = getConversationViewFragment();
if (op.mBatch) {
// Show undo bar in the conversation list.
params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
@@ -497,15 +501,11 @@
// Show undo bar in the conversation.
params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
- if (convView != null) {
- params.width = convView.getView().getWidth();
- }
+ params.width = mLayout.getConversationView().getWidth();
}
mUndoBarView.setLayoutParams(params);
- if (convView != null) {
- mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
- convList.getAnimatedAdapter());
- }
+ mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
+ convList.getAnimatedAdapter());
break;
}
}
diff --git a/src/com/android/mail/ui/TwoPaneLayout.java b/src/com/android/mail/ui/TwoPaneLayout.java
index 146a608..a3ec263 100644
--- a/src/com/android/mail/ui/TwoPaneLayout.java
+++ b/src/com/android/mail/ui/TwoPaneLayout.java
@@ -534,6 +534,10 @@
mListPaint.setAntiAlias(true);
}
+ public View getConversationView() {
+ return mConversationView;
+ }
+
private boolean isAnimatingFade() {
return mAnimatingFade;
}
diff --git a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
new file mode 100644
index 0000000..ff747c0
--- /dev/null
+++ b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v13.app.FragmentCompat;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.support.v4.util.SparseArrayCompat;
+import android.support.v4.view.PagerAdapter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Forked from support lib's {@link FragmentStatePagerAdapter}, with some minor
+ * changes that couldn't be accomplished through subclassing:
+ * <ul>
+ * <li>optionally disable stateful behavior when paging (controlled by {@link #mEnableSavedStates}),
+ * for situations where state save/restore when paging is unnecessary</li>
+ * <li>override-able {@link #setItemVisible(Fragment, boolean)} method for subclasses to
+ * add supplemental handling of visibility hints manually on pre-v15 devices</li>
+ * <li>add support to handle data set changes that cause item positions to change</li>
+ * </ul>
+ */
+public abstract class FragmentStatePagerAdapter2 extends PagerAdapter {
+ private static final String TAG = "FragmentStatePagerAdapter";
+ private static final boolean DEBUG = false;
+
+ private final FragmentManager mFragmentManager;
+ private FragmentTransaction mCurTransaction = null;
+
+ private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
+ private SparseArrayCompat<Fragment> mFragments = new SparseArrayCompat<Fragment>();
+ private Fragment mCurrentPrimaryItem = null;
+
+ private boolean mEnableSavedStates;
+
+ public FragmentStatePagerAdapter2(FragmentManager fm) {
+ this(fm, true);
+ }
+
+ public FragmentStatePagerAdapter2(FragmentManager fm, boolean enableSavedStates) {
+ mFragmentManager = fm;
+ mEnableSavedStates = enableSavedStates;
+ }
+
+ /**
+ * Return the Fragment associated with a specified position.
+ */
+ public abstract Fragment getItem(int position);
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ // If we already have this item instantiated, there is nothing
+ // to do. This can happen when we are restoring the entire pager
+ // from its saved state, where the fragment manager has already
+ // taken care of restoring the fragments we previously had instantiated.
+ final Fragment existing = mFragments.get(position);
+ if (existing != null) {
+ return existing;
+ }
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+
+ Fragment fragment = getItem(position);
+ if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+ if (mEnableSavedStates && mSavedState.size() > position) {
+ Fragment.SavedState fss = mSavedState.get(position);
+ if (fss != null) {
+ fragment.setInitialSavedState(fss);
+ }
+ }
+ if (fragment != mCurrentPrimaryItem) {
+ setItemVisible(fragment, false);
+ }
+ mFragments.put(position, fragment);
+ mCurTransaction.add(container.getId(), fragment);
+
+ return fragment;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ + " v=" + ((Fragment)object).getView());
+ if (mEnableSavedStates) {
+ while (mSavedState.size() <= position) {
+ mSavedState.add(null);
+ }
+ mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
+ }
+ mFragments.delete(position);
+
+ mCurTransaction.remove(fragment);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+ if (fragment != mCurrentPrimaryItem) {
+ if (mCurrentPrimaryItem != null) {
+ setItemVisible(mCurrentPrimaryItem, false);
+ }
+ if (fragment != null) {
+ setItemVisible(fragment, true);
+ }
+ mCurrentPrimaryItem = fragment;
+ }
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ if (mCurTransaction != null) {
+ mCurTransaction.commitAllowingStateLoss();
+ mCurTransaction = null;
+ mFragmentManager.executePendingTransactions();
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return ((Fragment)object).getView() == view;
+ }
+
+ @Override
+ public Parcelable saveState() {
+ Bundle state = null;
+ if (mEnableSavedStates && mSavedState.size() > 0) {
+ state = new Bundle();
+ Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
+ mSavedState.toArray(fss);
+ state.putParcelableArray("states", fss);
+ }
+ for (int i=0; i<mFragments.size(); i++) {
+ final int pos = mFragments.keyAt(i);
+ final Fragment f = mFragments.valueAt(i);
+ if (state == null) {
+ state = new Bundle();
+ }
+ String key = "f" + pos;
+ mFragmentManager.putFragment(state, key, f);
+ }
+ return state;
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ if (state != null) {
+ Bundle bundle = (Bundle)state;
+ bundle.setClassLoader(loader);
+ mFragments.clear();
+ if (mEnableSavedStates) {
+ Parcelable[] fss = bundle.getParcelableArray("states");
+ mSavedState.clear();
+ if (fss != null) {
+ for (int i=0; i<fss.length; i++) {
+ mSavedState.add((Fragment.SavedState)fss[i]);
+ }
+ }
+ }
+ Iterable<String> keys = bundle.keySet();
+ for (String key: keys) {
+ if (key.startsWith("f")) {
+ int index = Integer.parseInt(key.substring(1));
+ Fragment f = mFragmentManager.getFragment(bundle, key);
+ if (f != null) {
+ setItemVisible(f, false);
+ mFragments.put(index, f);
+ } else {
+ Log.w(TAG, "Bad fragment at key " + key);
+ }
+ }
+ }
+ }
+ }
+
+ public void setItemVisible(Fragment item, boolean visible) {
+ FragmentCompat.setMenuVisibility(item, visible);
+ FragmentCompat.setUserVisibleHint(item, visible);
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ super.notifyDataSetChanged();
+
+ // update positions in mFragments
+ for (int i=0; i<mFragments.size(); i++) {
+ final int oldPos = mFragments.keyAt(i);
+ final Fragment f = mFragments.valueAt(i);
+ final int newPos = getItemPosition(f);
+
+ if (newPos >= 0 && newPos != oldPos) {
+ // move
+ mFragments.removeAt(i);
+ mFragments.put(newPos, f);
+ } else if (newPos == POSITION_NONE) {
+ // remove
+ mFragments.removeAt(i);
+ }
+ }
+ }
+}