Fist cut of nested folders
1. Rename FolderListFragment.FolderListSelectionListener to
FolderSelector.
2. Allow special views to be tapped; all existing special views
disallow taps.
3. Allow nested folders in ConversationListFragment. The adapter is
responsible for populating the nested folders above the
conversations.
4. Add a nested folder special item for the conversation list. This
reuses current resources and is particularly ugly. The ugliness
will be fixed once we have a UX spec and real assets.
5. The child folders are loaded through an ObjectCursorLoader in the
ConversationListFragment.
Change-Id: I5eb566d7a1f87c1a11fc6961378d00650a27007d
diff --git a/res/layout/nested_folder.xml b/res/layout/nested_folder.xml
new file mode 100644
index 0000000..d93765b
--- /dev/null
+++ b/res/layout/nested_folder.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<!-- View that displays folders that are contained in a folder. These are shown at the top of
+ the conversation list. Email has them, Gmail doesn't currently. -->
+<com.android.mail.ui.NestedFolderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="@dimen/folder_list_item_minimum_height"
+ android:background="@drawable/folder_item" >
+
+ <!--This is a rough layout. We don't have UX specs yet, so all the values are hardcoded.
+ Also, it looks totally ugly. The ugliness is intentional.-->
+ <RelativeLayout
+ android:id="@+id/swipeable_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ >
+
+ <ImageView
+ android:id="@+id/nested_folder_icon"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:src="@drawable/ic_menu_folders_holo_light"
+ android:layout_alignParentLeft="true"
+ android:contentDescription="@string/folder_icon_desc"
+ android:layout_centerVertical="true"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:id="@+id/nested_folder_name"
+ android:includeFontPadding="false"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:textColor="@color/folder_item_text_color"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_centerVertical="true"
+ android:layout_height="match_parent"
+ android:layout_toRightOf="@id/nested_folder_icon" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/nested_folder_unread"
+ style="@style/UnreadCount"
+ android:layout_alignBaseline="@id/nested_folder_name"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="16dp"
+ />
+ </RelativeLayout>
+</com.android.mail.ui.NestedFolderView>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4c6ed01..86e05e9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -911,4 +911,6 @@
<string name="drawer_close">Close navigation drawer</string>
<string name="conversation_photo_welcome_text">Touch a sender image to select that conversation.</string>
+ <!-- Content description for the folder icon for nested folders. -->
+ <string name="folder_icon_desc">Folder icon</string>
</resources>
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 580679c..993d5d4 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -58,8 +58,8 @@
*/
public interface ActivityController extends LayoutListener,
ModeChangeListener, ConversationListCallbacks,
- FolderChangeListener, ConversationSetObserver, ConversationListener,
- FolderListFragment.FolderListSelectionListener, HelpCallback, UndoListener,
+ FolderChangeListener, ConversationSetObserver, ConversationListener, FolderSelector,
+ HelpCallback, UndoListener,
ConversationUpdater, ErrorListener, FolderController, AccountController,
ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener,
RecentFolderController, UpOrBackController {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fd387a6..1eba5a0 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -23,6 +23,7 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
+import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
@@ -37,6 +38,7 @@
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.ConversationItemViewCoordinates;
import com.android.mail.browse.SwipeableConversationItemView;
+import com.android.mail.content.ObjectCursor;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
@@ -46,10 +48,13 @@
import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -134,10 +139,11 @@
/**
* The next action to perform. Do not read or write this. All accesses should
- * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
- * previous action, if any.
+ * be in {@link #performAndSetNextAction(SwipeableListView.ListItemsRemovedListener)} which
+ * commits the previous action, if any.
*/
private ListItemsRemovedListener mPendingDestruction;
+
/**
* A destructive action that refreshes the list and performs no other action.
*/
@@ -169,33 +175,38 @@
}
};
- private final List<ConversationSpecialItemView> mSpecialViews;
- private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
+ /**
+ * A list of all views that are not conversations. These include temporary views from
+ * {@link #mFleetingViews} and child folders from {@link #mFolderViews}.
+ */
+ private final SparseArray<ConversationSpecialItemView> mSpecialViews;
private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
new SparseArray<ConversationItemViewCoordinates>();
- private final void setAccount(Account newAccount) {
+ /**
+ * Temporary views insert at specific positions relative to conversations. These can be
+ * related to showing new features (on-boarding) or showing information about new mailboxes
+ * that have been added by the system.
+ */
+ private final List<ConversationSpecialItemView> mFleetingViews;
+
+ /** List of all child folders for this folder. */
+ private List<NestedFolderView> mFolderViews;
+
+ private void setAccount(Account newAccount) {
mAccount = newAccount;
mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
}
- /**
- * Used only for debugging.
- */
private static final String LOG_TAG = LogTag.getLogTag();
private static final int INCREASE_WAIT_COUNT = 2;
public AnimatedAdapter(Context context, ConversationCursor cursor,
ConversationSelectionSet batch, ControllableActivity activity,
- SwipeableListView listView) {
- this(context, cursor, batch, activity, listView, null);
- }
-
- public AnimatedAdapter(Context context, ConversationCursor cursor,
- ConversationSelectionSet batch, ControllableActivity activity,
- SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
+ SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
+ final ObjectCursor<Folder> childFolders) {
super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
mContext = context;
mBatchConversations = batch;
@@ -203,27 +214,61 @@
mActivity = activity;
mShowFooter = false;
mListView = listView;
+ mFolderViews = getNestedFolders(childFolders);
+
mHandler = new Handler();
if (sDismissAllShortDelay == -1) {
- sDismissAllShortDelay =
- context.getResources()
- .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
- sDismissAllLongDelay =
- context.getResources()
- .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
+ final Resources r = context.getResources();
+ sDismissAllShortDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
+ sDismissAllLongDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
}
- mSpecialViews =
- specialViews == null ? new ArrayList<ConversationSpecialItemView>(0)
- : new ArrayList<ConversationSpecialItemView>(specialViews);
- mSpecialViewPositions = new SparseArray<ConversationSpecialItemView>(mSpecialViews.size());
+ if (specialViews != null) {
+ mFleetingViews = new ArrayList<ConversationSpecialItemView>(specialViews);
+ } else {
+ mFleetingViews = new ArrayList<ConversationSpecialItemView>(0);
+ }
+ /** Total number of special views */
+ final int size = mFleetingViews.size() + mFolderViews.size();
+ mSpecialViews = new SparseArray<ConversationSpecialItemView>(size);
- for (final ConversationSpecialItemView view : mSpecialViews) {
+ // Only set the adapter in teaser views. Folder views don't care about the adapter.
+ for (final ConversationSpecialItemView view : mFleetingViews) {
view.setAdapter(this);
}
-
updateSpecialViews();
}
+ /**
+ * Returns a list containing views for all the nested folders.
+ * @param cursor cursor containing the folders nested within the current folder
+ * @return a list, possibly empty of the views representing the folders.
+ */
+ private List<NestedFolderView> getNestedFolders (final ObjectCursor<Folder> cursor) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ // The cursor has nothing valid. Return an empty list.
+ return ImmutableList.of();
+ }
+
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ final List<NestedFolderView> folders = new ArrayList<NestedFolderView>(cursor.getCount());
+ do {
+ final NestedFolderView view =
+ (NestedFolderView) inflater.inflate(R.layout.nested_folder, null);
+ view.setFolder(cursor.getModel());
+ folders.add(view);
+ } while (cursor.moveToNext());
+ return folders;
+ }
+
+ /**
+ * Updates the list of folders for the current list with the cursor provided here.
+ * @param childFolders A cursor containing child folders for the current folder.
+ */
+ public void updateNestedFolders (ObjectCursor<Folder> childFolders) {
+ mFolderViews = getNestedFolders(childFolders);
+ notifyDataSetChanged();
+ }
+
public void cancelDismissCounter() {
cancelLeaveBehindFadeInAnimation();
mHandler.removeCallbacks(mCountDown);
@@ -245,8 +290,8 @@
@Override
public int getCount() {
- // mSpecialViewPositions only contains the views that are currently being displayed
- final int specialViewCount = mSpecialViewPositions.size();
+ // mSpecialViews only contains the views that are currently being displayed
+ final int specialViewCount = mSpecialViews.size();
final int count = super.getCount() + specialViewCount;
return mShowFooter ? count + 1 : count;
@@ -339,7 +384,7 @@
// types. In a future release, use position/id map to try to make
// this cleaner / faster to determine if the view is animating.
return TYPE_VIEW_DONT_RECYCLE;
- } else if (mSpecialViewPositions.get(position) != null) {
+ } else if (mSpecialViews.get(position) != null) {
// Don't recycle the special views
return TYPE_VIEW_DONT_RECYCLE;
}
@@ -412,12 +457,12 @@
}
// Check if this is a special view
- final View specialView = (View) mSpecialViewPositions.get(position);
+ final View specialView = (View) mSpecialViews.get(position);
if (specialView != null) {
return specialView;
}
- ConversationCursor cursor = (ConversationCursor) getItem(position);
+ final ConversationCursor cursor = (ConversationCursor) getItem(position);
final Conversation conv = cursor.getConversation();
// Notify the provider of this change in the position of Conversation cursor
@@ -616,7 +661,7 @@
@Override
public long getItemId(int position) {
if (mShowFooter && position == getCount() - 1
- || mSpecialViewPositions.get(position) != null) {
+ || mSpecialViews.get(position) != null) {
return -1;
}
final int cursorPos = position - getPositionOffset(position);
@@ -668,9 +713,7 @@
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- SwipeableConversationItemView view = new SwipeableConversationItemView(context,
- mAccount.name);
- return view;
+ return new SwipeableConversationItemView(context, mAccount.name);
}
@Override
@@ -700,8 +743,8 @@
public Object getItem(int position) {
if (mShowFooter && position == getCount() - 1) {
return mFooter;
- } else if (mSpecialViewPositions.get(position) != null) {
- return mSpecialViewPositions.get(position);
+ } else if (mSpecialViews.get(position) != null) {
+ return mSpecialViews.get(position);
}
return super.getItem(position - getPositionOffset(position));
}
@@ -739,7 +782,7 @@
* @param next The next action that is to be performed, possibly null (if no next action is
* needed).
*/
- private final void performAndSetNextAction(ListItemsRemovedListener next) {
+ private void performAndSetNextAction(ListItemsRemovedListener next) {
if (mPendingDestruction != null) {
mPendingDestruction.onListItemsRemoved();
}
@@ -763,28 +806,21 @@
@Override
public boolean areAllItemsEnabled() {
- // The animating positions are not enabled.
+ // The animating items and some special views are not enabled.
return false;
}
@Override
public boolean isEnabled(final int position) {
- if (mSpecialViewPositions.get(position) != null) {
- // This is a special view
- return false;
+ final ConversationSpecialItemView view = mSpecialViews.get(position);
+ if (view != null) {
+ final boolean enabled = view.acceptsUserTaps();
+ LogUtils.d(LOG_TAG, "AA.isEnabled(%d) = %b", position, enabled);
+ return enabled;
}
-
return !isPositionDeleting(position) && !isPositionUndoing(position);
}
- public void showFooter() {
- setFooterVisibility(true);
- }
-
- public void hideFooter() {
- setFooterVisibility(false);
- }
-
public void setFooterVisibility(boolean show) {
if (mShowFooter != show) {
mShowFooter = show;
@@ -836,8 +872,8 @@
public void onRestoreInstanceState(Bundle outState) {
if (outState.containsKey(LAST_DELETING_ITEMS)) {
final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
- for (int i = 0; i < lastDeleting.length; i++) {
- mLastDeletingItems.add(lastDeleting[i]);
+ for (final long aLastDeleting : lastDeleting) {
+ mLastDeletingItems.add(aLastDeleting);
}
}
if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
@@ -910,24 +946,40 @@
}
}
+ /**
+ * Updates special (non-conversation view) when either {@link #mFolderViews} or
+ * {@link #mFleetingViews} changed
+ */
private void updateSpecialViews() {
- mSpecialViewPositions.clear();
+ // We recreate all the special views using mFolderViews and mFleetingViews (in that order).
+ mSpecialViews.clear();
- for (int i = 0; i < mSpecialViews.size(); i++) {
- final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+ int folderCount = 0;
+ // Nested folders are added initially. They don't specify positions: we put them at the
+ // very top.
+ for (final NestedFolderView view : mFolderViews) {
+ mSpecialViews.put(folderCount, view);
+ folderCount++;
+ }
+
+ // Fleeting (temporary) views go after this. They specify a position,which is 0-indexed and
+ // has to be adjusted for the number of folders above it.
+ for (final ConversationSpecialItemView specialView : mFleetingViews) {
specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
if (specialView.getShouldDisplayInList()) {
- int position = specialView.getPosition();
+ // If the special view asks for position 0, it wants to be at the top. However,
+ // if there are already 3 folders above it, the real position it needs is 0+3 (4th
+ // from top, since everything is 0-indexed).
+ int position = (specialView.getPosition() + folderCount);
// insert the special view into the position, but if there is
// already an item occupying that position, move that item back
// one position, and repeat
ConversationSpecialItemView insert = specialView;
while (insert != null) {
- final ConversationSpecialItemView kickedOut = mSpecialViewPositions.get(
- position);
- mSpecialViewPositions.put(position, insert);
+ final ConversationSpecialItemView kickedOut = mSpecialViews.get(position);
+ mSpecialViews.put(position, insert);
insert = kickedOut;
position++;
}
@@ -968,9 +1020,8 @@
public int getPositionOffset(final int position) {
int offset = 0;
- for (int i = 0; i < mSpecialViewPositions.size(); i++) {
- final int key = mSpecialViewPositions.keyAt(i);
- final ConversationSpecialItemView specialView = mSpecialViewPositions.get(key);
+ for (int i = 0, size = mSpecialViews.size(); i < size; i++) {
+ final int key = mSpecialViews.keyAt(i);
if (key <= position) {
offset++;
}
@@ -980,14 +1031,15 @@
}
public void cleanup() {
- for (final ConversationSpecialItemView view : mSpecialViews) {
+ // Only clean up teaser views. Folder views don't care about clean up.
+ for (final ConversationSpecialItemView view : mFleetingViews) {
view.cleanup();
}
}
public void onConversationSelected() {
- for (int i = 0; i < mSpecialViews.size(); i++) {
- final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+ // Only notify teaser views. Folder views don't care about selected conversations.
+ for (final ConversationSpecialItemView specialView : mFleetingViews) {
specialView.onConversationSelected();
}
}
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index fae8665..9bd563b 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -72,7 +72,7 @@
* fragment so that activity controllers can track the last folder list
* pushed for hierarchical folders.
*/
- FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
+ FolderSelector getFolderSelector();
/**
* Get the folder currently being accessed by the activity.
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 16fc684..b11cadd 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -21,15 +21,15 @@
import android.app.Activity;
import android.app.ListFragment;
+import android.app.LoaderManager;
import android.content.Context;
+import android.content.Loader;
import android.content.res.Resources;
import android.database.DataSetObserver;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.format.DateUtils;
-import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -46,6 +46,8 @@
import com.android.mail.browse.ConversationItemViewModel;
import com.android.mail.browse.ConversationListFooterView;
import com.android.mail.browse.ToggleableItem;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
@@ -147,6 +149,52 @@
private int mConversationCursorHash;
/**
+ * If the current list is for a folder with children, this set of loader callbacks will
+ * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
+ * list.
+ */
+ private final class ChildFolderLoads
+ implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
+ /** Load all child folders for the current folder. */
+ private static final int LOADER_CHIDREN = 0;
+ public static final String CHILD_URI = "arg-child-uri";
+ private final String[] projection = UIProvider.FOLDERS_PROJECTION;
+
+ @Override
+ public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
+ if (id != LOADER_CHIDREN) {
+ throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
+ }
+ final Uri childUri = Uri.parse(args.getString(CHILD_URI));
+ return new ObjectCursorLoader<Folder>(
+ getActivity(), childUri, projection, Folder.FACTORY);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
+ if (data != null && data.getCount() >= 0 && mListAdapter != null) {
+ mListAdapter.updateNestedFolders(data);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
+ // Do nothing.
+ }
+ }
+
+ /** Callbacks to handle creating a loader and receiving child folders from it. */
+ private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
+
+ /**
+ * Include all the folders at the cursor provided here in the conversation list.
+ * @param cursor The cursor containing child folders for the current folder.
+ */
+ private void showChildFolders(ObjectCursor<Folder> cursor) {
+
+ }
+
+ /**
* Constructor needs to be public to handle orientation changes and activity
* lifecycle events.
*/
@@ -265,26 +313,35 @@
mFooterView.setClickListener(mActivity);
mConversationListView.setActivity(mActivity);
final ConversationCursor conversationCursor = getConversationListCursor();
+ final LoaderManager manager = getLoaderManager();
+
+ // If this a parent folder, load all the child folders.
+ if (mViewContext.folder.hasChildren) {
+ final Uri childUri = mViewContext.folder.childFoldersListUri;
+ final Bundle args = new Bundle();
+ args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
+ manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
+ }
final ConversationListHelper helper = mActivity.getConversationListHelper();
final List<ConversationSpecialItemView> specialItemViews = helper != null ?
ImmutableList.copyOf(helper.makeConversationListSpecialViews(
- getActivity(), mAccount, mActivity.getFolderListSelectionListener()))
+ activity, mAccount, mActivity.getFolderSelector()))
: null;
if (specialItemViews != null) {
// Attach to the LoaderManager
for (final ConversationSpecialItemView view : specialItemViews) {
- view.bindLoaderManager(getLoaderManager());
+ view.bindLoaderManager(manager);
}
}
mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
- mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
+ mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
mSelectedSet = mActivity.getSelectedSet();
mListView.setSelectionSet(mSelectedSet);
- mListAdapter.hideFooter();
+ mListAdapter.setFooterVisibility(false);
mFolderObserver = new FolderObserver(){
@Override
public void onChanged(Folder newFolder) {
@@ -521,19 +578,22 @@
*/
@Override
public void onListItemClick(ListView l, View view, int position, long id) {
- // Ignore anything that is not a conversation item. Could be a footer.
- // If we are using a keyboard, the highlighted item is the parent;
- // otherwise, this is a direct call from the ConverationItemView
- if (!(view instanceof ToggleableItem)) {
- return;
- }
- boolean showSenderImage = (mAccount.settings.convListIcon ==
- ConversationListIcon.SENDER_IMAGE);
- if (!showSenderImage && !mSelectedSet.isEmpty()) {
- ToggleableItem v = (ToggleableItem) view;
- v.toggleSelectedState();
+ if (view instanceof NestedFolderView) {
+ final FolderSelector selector = mActivity.getFolderSelector();
+ selector.onFolderSelected(((NestedFolderView) view).getFolder());
+ } else if (view instanceof ToggleableItem) {
+ final boolean showSenderImage =
+ (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
+ if (!showSenderImage && !mSelectedSet.isEmpty()) {
+ ((ToggleableItem) view).toggleSelectedState();
+ } else {
+ viewConversation(position);
+ }
} else {
- viewConversation(position);
+ // Ignore anything that is not a conversation item. Could be a footer.
+ // If we are using a keyboard, the highlighted item is the parent;
+ // otherwise, this is a direct call from the ConverationItemView
+ return;
}
// When a new list item is clicked, commit any existing leave behind
// items. Wait until we have opened the desired conversation to cause
diff --git a/src/com/android/mail/ui/ConversationListHelper.java b/src/com/android/mail/ui/ConversationListHelper.java
index 05c27d0..f7a6862 100644
--- a/src/com/android/mail/ui/ConversationListHelper.java
+++ b/src/com/android/mail/ui/ConversationListHelper.java
@@ -21,7 +21,6 @@
import android.content.Context;
import com.android.mail.providers.Account;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import java.util.ArrayList;
@@ -30,7 +29,7 @@
* Creates a list of newly created special views.
*/
public ArrayList<ConversationSpecialItemView> makeConversationListSpecialViews(Context context,
- Account account, FolderListSelectionListener listener) {
+ Account account, FolderSelector listener) {
// TODO: Move conversation photo teaser view here once
// getConversationListIcon() is moved out of Persistence
return Lists.newArrayList();
diff --git a/src/com/android/mail/ui/ConversationPhotoTeaserView.java b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
index 84b710d..9d5f437 100644
--- a/src/com/android/mail/ui/ConversationPhotoTeaserView.java
+++ b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
@@ -135,6 +135,12 @@
}
@Override
+ public boolean acceptsUserTaps() {
+ // No, we don't allow user taps.
+ return false;
+ }
+
+ @Override
public void dismiss() {
setDismissed();
startDestroyAnimation();
diff --git a/src/com/android/mail/ui/ConversationSpecialItemView.java b/src/com/android/mail/ui/ConversationSpecialItemView.java
index 097641e..e0b221e 100644
--- a/src/com/android/mail/ui/ConversationSpecialItemView.java
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -34,8 +34,17 @@
*/
void onUpdate(String account, Folder folder, ConversationCursor cursor);
+ /**
+ * Returns whether this view is to be displayed in the list or not. A view can be added freely
+ * and it might decide to disable itself by returning false here.
+ * @return true if this view should be displayed, false otherwise.
+ */
boolean getShouldDisplayInList();
+ /**
+ * Returns the position (0 indexed) where this element expects to be inserted.
+ * @return
+ */
int getPosition();
void setAdapter(AnimatedAdapter adapter);
@@ -51,4 +60,7 @@
* Called when a regular conversation item was clicked.
*/
void onConversationSelected();
+
+ /** Returns whether this special view is enabled (= accepts user taps). */
+ boolean acceptsUserTaps();
}
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index a8a718a..120240c 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -103,7 +103,7 @@
/** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
private ArrayList<Integer> mExcludedFolderTypes;
/** Object that changes folders on our behalf. */
- private FolderListSelectionListener mFolderChanger;
+ private FolderSelector mFolderChanger;
/** Object that changes accounts on our behalf */
private AccountController mAccountController;
@@ -312,7 +312,7 @@
setSelectedAccount(newAccount);
}
};
- mFolderChanger = mActivity.getFolderListSelectionListener();
+ mFolderChanger = mActivity.getFolderSelector();
if (accountController != null) {
// Current account and its observer.
setSelectedAccount(mAccountObserver.initialize(accountController));
@@ -1196,10 +1196,6 @@
}
}
- public interface FolderListSelectionListener {
- public void onFolderSelected(Folder folder);
- }
-
/**
* Get whether the FolderListFragment is currently showing the hierarchy
* under a single parent.
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index 6a121fb..8b8d0a8 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -37,7 +37,6 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.Folder;
import com.android.mail.providers.FolderWatcher;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -53,7 +52,7 @@
*/
public class FolderSelectionActivity extends Activity implements OnClickListener,
DialogInterface.OnClickListener, FolderChangeListener, ControllableActivity,
- FolderListSelectionListener {
+ FolderSelector {
public static final String EXTRA_ACCOUNT_SHORTCUT = "account-shortcut";
private static final String LOG_TAG = LogTag.getLogTag();
@@ -364,7 +363,7 @@
}
@Override
- public FolderListSelectionListener getFolderListSelectionListener() {
+ public FolderSelector getFolderSelector() {
return this;
}
diff --git a/src/com/android/mail/ui/FolderSelector.java b/src/com/android/mail/ui/FolderSelector.java
new file mode 100644
index 0000000..6523a34
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelector.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import com.android.mail.providers.Folder;
+
+/**
+ * Interface that permits elements to implement selecting a folder.
+ * The single method {@link #onFolderSelected(com.android.mail.providers.Folder)} defines what
+ * happens when a folder is selected.
+ */
+public interface FolderSelector {
+ /**
+ * Selects the folder provided as an argument here. This corresponds to the user
+ * selecting a folder in the UI element, either for creating a widget/shortcut (as in the
+ * case of {@link FolderSelectionActivity} or for viewing the contents of
+ * the folder (as in the case of {@link AbstractActivityController}.
+ * @param folder
+ */
+ public void onFolderSelected(Folder folder);
+}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index bc0c693..2cf2256 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -36,7 +36,6 @@
import com.android.mail.compose.ComposeActivity;
import com.android.mail.providers.Folder;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.utils.StorageLowState;
import com.android.mail.utils.Utils;
@@ -308,7 +307,7 @@
}
@Override
- public FolderListSelectionListener getFolderListSelectionListener() {
+ public FolderSelector getFolderSelector() {
return mController;
}
diff --git a/src/com/android/mail/ui/NestedFolderView.java b/src/com/android/mail/ui/NestedFolderView.java
new file mode 100644
index 0000000..e9c3afb
--- /dev/null
+++ b/src/com/android/mail/ui/NestedFolderView.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Folder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * For folders that might contain other folders, we show the nested folders within this view.
+ * Tapping on this opens the folder.
+ */
+public class NestedFolderView extends LinearLayout implements ConversationSpecialItemView,
+ SwipeableItemView {
+ protected static final String LOG_TAG = LogTag.getLogTag();
+ /**
+ * The actual view that is displayed and is perhaps swiped away. We don't allow swiping,
+ * but this is required by the {@link SwipeableItemView} interface.
+ */
+ private View mSwipeableContent;
+ /** The folder this view represents */
+ private Folder mFolder;
+
+ public NestedFolderView(Context context) {
+ super(context);
+ }
+
+ public NestedFolderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NestedFolderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSwipeableContent = findViewById(R.id.swipeable_content);
+ }
+
+ @Override
+ public void onUpdate(String s, Folder folder, ConversationCursor conversationCursor) {
+ // Do nothing. We don't care about the change to the conversation cursor here.
+ // Nested folders only care if they were removed from the parent folder,
+ // so supposing we should check for that here.
+ }
+
+ /**
+ * Sets the folder associated with this view. This method is meant to be called infrequently,
+ * since we assume that a new view will be created when the unread count changes.
+ * @param folder the folder that this view represents.
+ */
+ public void setFolder(Folder folder) {
+ mFolder = folder;
+ // Since we assume that setFolder will be called infrequently (once currently),
+ // we don't bother saving the textviews for folder name and folder unread count. If we
+ // find that setFolder gets called repeatedly, it might be prudent to remember the
+ // references to these textviews, making setFolder slightly faster.
+ TextView t = (TextView) findViewById(R.id.nested_folder_name);
+ t.setText(folder.name);
+ t = (TextView) findViewById(R.id.nested_folder_unread);
+ t.setText("" + folder.unreadCount);
+ }
+
+ /**
+ * Returns the folder associated with this view
+ * @return a folder that this view represents.
+ */
+ public Folder getFolder() {
+ return mFolder;
+ }
+
+ @Override
+ public boolean getShouldDisplayInList() {
+ // Nested folders once created are always displayed in the list.
+ return true;
+ }
+
+ @Override
+ public int getPosition() {
+ // We only have one element, and that's always at the top for now.
+ return 0;
+ }
+
+ @Override
+ public void setAdapter(AnimatedAdapter animatedAdapter) {
+ // Do nothing, since the adapter creates these views.
+ }
+
+ @Override
+ public void bindLoaderManager(LoaderManager loaderManager) {
+ // Do nothing. We don't need the loader manager.
+ }
+
+ @Override
+ public void cleanup() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onConversationSelected() {
+ // Do nothing. We don't care if conversations are selected.
+ }
+
+ @Override
+ public boolean acceptsUserTaps() {
+ return true;
+ }
+
+ @Override
+ public SwipeableView getSwipeableView() {
+ return SwipeableView.from(mSwipeableContent);
+ }
+
+ @Override
+ public boolean canChildBeDismissed() {
+ // The folders can never be dismissed, return false.
+ return false;
+ }
+
+ @Override
+ public void dismiss() {
+ /** How did this happen? We returned false in {@link #canChildBeDismissed()} so this
+ * method should never be called. */
+ LogUtils.wtf(LOG_TAG, "NestedFolderView.dismiss() called. Not expected.");
+ }
+
+ @Override
+ public float getMinAllowScrollDistance() {
+ return -1;
+ }
+}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index ed62708..3e8ff2b 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -201,7 +201,8 @@
final int transition = mConversationListNeverShown
? FragmentTransaction.TRANSIT_FRAGMENT_FADE
: FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
- Fragment conversationListFragment = ConversationListFragment.newInstance(listContext);
+ final Fragment conversationListFragment =
+ ConversationListFragment.newInstance(listContext);
if (!inInbox(mAccount, listContext)) {
// Maintain fragment transaction history so we can get back to the
diff --git a/src/com/android/mail/ui/SwipeableItemView.java b/src/com/android/mail/ui/SwipeableItemView.java
index 9bf7f34..3a4a13f 100644
--- a/src/com/android/mail/ui/SwipeableItemView.java
+++ b/src/com/android/mail/ui/SwipeableItemView.java
@@ -29,6 +29,11 @@
public void dismiss();
+ /**
+ * Returns the minimum allowed displacement in the Y axis that is considered a scroll. After
+ * this displacement, all future events are considered scroll events rather than swipes.
+ * @return
+ */
public float getMinAllowScrollDistance();
public static class SwipeableView {
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 5294a61..f67f30a 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -70,7 +70,8 @@
FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
// Use cross fading animation.
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
- Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
+ final Fragment conversationListFragment =
+ ConversationListFragment.newInstance(mConvListContext);
fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
TAG_CONVERSATION_LIST);
fragmentTransaction.commitAllowingStateLoss();
@@ -167,26 +168,18 @@
mViewMode.enterConversationListMode();
}
- if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
- // Replace this fragment with a new FolderListFragment
- // showing this folder's children if we are not already looking
- // at the child view for this folder.
- createFolderTree(folder);
+ if (folder.hasChildren) {
// Show the up affordance when digging into child folders.
mActionBarView.setBackButton();
- } else {
- setHierarchyFolder(folder);
}
+ setHierarchyFolder(folder);
super.onFolderSelected(folder);
}
private void goUpFolderHierarchy(Folder current) {
- Folder parent = current.parent;
- if (parent.parent != null) {
- createFolderTree(parent.parent);
- // Show the up affordance when digging into child folders.
- mActionBarView.setBackButton();
- } else {
+ // If the current folder is a child, up should show the parent folder.
+ final Folder parent = current.parent;
+ if (parent != null) {
onFolderSelected(parent);
}
}