OSDN Git Service

Recents in create, uniform item layouts.
authorJeff Sharkey <jsharkey@android.com>
Tue, 6 Aug 2013 23:26:14 +0000 (16:26 -0700)
committerJeff Sharkey <jsharkey@android.com>
Wed, 7 Aug 2013 01:16:21 +0000 (18:16 -0700)
Show recent directories in a separate fragment, and save as serialized
versions of new DirectoryStack. Cleaner behavior around recents and
search, instead of treating them as pseudo-Documents.

More uniform item layouts between list and grid, including both date
and size information, and originating storage root in recents. Avoid
clashing directory loaders by assigning unique numbers. Promote
list/grid switching up into activity.

Change-Id: I9a93460b896067ca036d7e772eeabde31face2e1

16 files changed:
packages/DocumentsUI/AndroidManifest.xml
packages/DocumentsUI/res/layout/fragment_roots.xml
packages/DocumentsUI/res/layout/item_doc_grid.xml
packages/DocumentsUI/res/layout/item_doc_list.xml
packages/DocumentsUI/res/menu/activity.xml
packages/DocumentsUI/res/menu/directory.xml [deleted file]
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java
packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java [new file with mode: 0644]
packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
packages/DocumentsUI/src/com/android/documentsui/model/Document.java
packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java [new file with mode: 0644]
packages/DocumentsUI/src/com/android/documentsui/model/Root.java

index b88099e..1d97161 100644 (file)
@@ -3,7 +3,10 @@
 
     <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
 
-    <application android:label="@string/app_label">
+    <application
+        android:label="@string/app_label"
+        android:supportsRtl="true">
+
         <activity
             android:name=".DocumentsActivity"
             android:finishOnCloseSystemDialogs="true"
index d772892..09782d9 100644 (file)
@@ -17,4 +17,5 @@
 <ListView xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@android:id/list"
     android:layout_width="match_parent"
-    android:layout_height="match_parent" />
+    android:layout_height="match_parent"
+    android:divider="@null" />
index caa9db6..ad8f51c 100644 (file)
@@ -16,7 +16,7 @@
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="140dip"
+    android:layout_height="160dip"
     android:paddingBottom="?android:attr/listPreferredItemPaddingEnd"
     android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
 
         android:foreground="@drawable/item_background"
         android:duplicateParentState="true">
 
-        <GridLayout
+        <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:columnCount="1">
+            android:orientation="vertical">
 
             <ImageView
                 android:id="@android:id/icon"
                 android:layout_width="match_parent"
                 android:layout_height="0dip"
-                android:layout_gravity="fill_vertical"
+                android:layout_weight="1"
                 android:background="#bbb"
                 android:scaleType="centerInside"
                 android:contentDescription="@null" />
             <TextView
                 android:id="@android:id/title"
                 android:layout_width="match_parent"
-                android:layout_marginStart="8dip"
-                android:layout_marginEnd="8dip"
-                android:layout_marginTop="8dip"
-                android:layout_marginBottom="8dip"
+                android:layout_height="wrap_content"
                 android:singleLine="true"
                 android:ellipsize="marquee"
+                android:paddingTop="6dp"
+                android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+                android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
                 android:textAppearance="?android:attr/textAppearanceSmall"
                 android:textAlignment="viewStart" />
 
-        </GridLayout>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:paddingBottom="6dp"
+                android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+                android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+                <ImageView
+                    android:id="@android:id/icon1"
+                    android:layout_width="24dip"
+                    android:layout_height="24dip"
+                    android:layout_marginEnd="6dip"
+                    android:scaleType="centerInside"
+                    android:contentDescription="@null" />
+
+                <TextView
+                    android:id="@android:id/summary"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginEnd="8dp"
+                    android:singleLine="true"
+                    android:ellipsize="marquee"
+                    android:textAlignment="viewStart"
+                    android:textAppearance="?android:attr/textAppearanceSmall" />
+
+                <TextView
+                    android:id="@+id/size"
+                    android:layout_width="70dp"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginEnd="8dp"
+                    android:singleLine="true"
+                    android:ellipsize="marquee"
+                    android:textAlignment="viewEnd"
+                    android:textAppearance="?android:attr/textAppearanceSmall" />
+
+                <TextView
+                    android:id="@+id/date"
+                    android:layout_width="70dp"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:singleLine="true"
+                    android:ellipsize="marquee"
+                    android:textAlignment="viewEnd"
+                    android:textAppearance="?android:attr/textAppearanceSmall" />
+
+            </LinearLayout>
+
+        </LinearLayout>
 
     </FrameLayout>
 
index 39e55be..bb27173 100644 (file)
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@drawable/item_background"
     android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
     android:paddingTop="8dip"
     android:paddingBottom="8dip"
-    android:columnCount="3">
+    android:orientation="horizontal">
 
     <ImageView
         android:id="@android:id/icon"
         android:layout_width="@android:dimen/app_icon_size"
         android:layout_height="@android:dimen/app_icon_size"
-        android:layout_rowSpan="2"
         android:layout_marginEnd="8dip"
+        android:layout_gravity="center_vertical"
         android:scaleType="centerInside"
         android:contentDescription="@null" />
 
-    <TextView
-        android:id="@android:id/title"
+    <LinearLayout
         android:layout_width="0dip"
-        android:layout_gravity="fill_horizontal"
-        android:layout_marginTop="2dip"
-        android:layout_columnSpan="2"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:textAlignment="viewStart" />
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:orientation="vertical">
 
-    <ImageView
-        android:id="@android:id/icon1"
-        android:layout_width="24dip"
-        android:layout_height="24dip"
-        android:layout_marginEnd="8dip"
-        android:visibility="gone"
-        android:scaleType="centerInside"
-        android:contentDescription="@null" />
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textAlignment="viewStart" />
+
+        <LinearLayout
+            android:id="@+id/line2"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@android:id/icon1"
+                android:layout_width="24dip"
+                android:layout_height="24dip"
+                android:layout_marginEnd="6dip"
+                android:scaleType="centerInside"
+                android:contentDescription="@null" />
+
+            <TextView
+                android:id="@android:id/summary"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="8dp"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:textAlignment="viewStart"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+
+            <TextView
+                android:id="@+id/size"
+                android:layout_width="70dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="8dp"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:textAlignment="viewEnd"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+
+            <TextView
+                android:id="@+id/date"
+                android:layout_width="70dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:textAlignment="viewEnd"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+
+        </LinearLayout>
 
-    <TextView
-        android:id="@android:id/summary"
-        android:layout_marginTop="2dip"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
+    </LinearLayout>
 
-</GridLayout>
+</LinearLayout>
index a0d03b2..d57f88a 100644 (file)
         android:showAsAction="always|collapseActionView"
         android:actionViewClass="android.widget.SearchView"
         android:imeOptions="actionSearch" />
+    <item
+        android:id="@+id/menu_grid"
+        android:title="@string/menu_grid"
+        android:icon="@drawable/ic_menu_grid"
+        android:showAsAction="ifRoom" />
+    <item
+        android:id="@+id/menu_list"
+        android:title="@string/menu_list"
+        android:icon="@drawable/ic_menu_list"
+        android:showAsAction="ifRoom" />
 </menu>
diff --git a/packages/DocumentsUI/res/menu/directory.xml b/packages/DocumentsUI/res/menu/directory.xml
deleted file mode 100644 (file)
index 12d0324..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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.
--->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item
-        android:id="@+id/menu_grid"
-        android:title="@string/menu_grid"
-        android:icon="@drawable/ic_menu_grid"
-        android:showAsAction="ifRoom" />
-    <item
-        android:id="@+id/menu_list"
-        android:title="@string/menu_list"
-        android:icon="@drawable/ic_menu_list"
-        android:showAsAction="ifRoom" />
-</menu>
index 1443f26..d986a51 100644 (file)
@@ -26,11 +26,12 @@ import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.text.format.DateUtils;
+import android.text.format.Formatter;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.ActionMode;
 import android.view.LayoutInflater;
 import android.view.Menu;
-import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,20 +47,21 @@ import android.widget.TextView;
 
 import com.android.documentsui.DocumentsActivity.DisplayState;
 import com.android.documentsui.model.Document;
+import com.android.documentsui.model.Root;
 import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
 
+import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Display the documents inside a single directory.
  */
 public class DirectoryFragment extends Fragment {
 
-    // TODO: show storage backend in item views when requested
-
     private ListView mListView;
     private GridView mGridView;
 
@@ -68,19 +70,35 @@ public class DirectoryFragment extends Fragment {
     public static final int TYPE_NORMAL = 1;
     public static final int TYPE_SEARCH = 2;
     public static final int TYPE_RECENT_OPEN = 3;
-    public static final int TYPE_RECENT_CREATE = 4;
 
     private int mType = TYPE_NORMAL;
 
     private DocumentsAdapter mAdapter;
     private LoaderCallbacks<List<Document>> mCallbacks;
 
+    private static final String EXTRA_TYPE = "type";
     private static final String EXTRA_URI = "uri";
 
-    private static final int LOADER_DOCUMENTS = 2;
+    private static AtomicInteger sLoaderId = new AtomicInteger(4000);
+
+    private final int mLoaderId = sLoaderId.incrementAndGet();
+
+    public static void showNormal(FragmentManager fm, Uri uri) {
+        show(fm, TYPE_NORMAL, uri);
+    }
+
+    public static void showSearch(FragmentManager fm, Uri uri, String query) {
+        final Uri searchUri = DocumentsContract.buildSearchUri(uri, query);
+        show(fm, TYPE_SEARCH, searchUri);
+    }
+
+    public static void showRecentsOpen(FragmentManager fm) {
+        show(fm, TYPE_RECENT_OPEN, null);
+    }
 
-    public static void show(FragmentManager fm, Uri uri) {
+    private static void show(FragmentManager fm, int type, Uri uri) {
         final Bundle args = new Bundle();
+        args.putInt(EXTRA_TYPE, type);
         args.putParcelable(EXTRA_URI, uri);
 
         final DirectoryFragment fragment = new DirectoryFragment();
@@ -97,12 +115,6 @@ public class DirectoryFragment extends Fragment {
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setHasOptionsMenu(true);
-    }
-
-    @Override
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final Context context = inflater.getContext();
@@ -118,19 +130,9 @@ public class DirectoryFragment extends Fragment {
         mGridView.setMultiChoiceModeListener(mMultiListener);
 
         mAdapter = new DocumentsAdapter();
-        updateMode();
 
         final Uri uri = getArguments().getParcelable(EXTRA_URI);
-
-        if (uri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
-            mType = TYPE_SEARCH;
-        } else if (RecentsProvider.buildRecentOpen().equals(uri)) {
-            mType = TYPE_RECENT_OPEN;
-        } else if (RecentsProvider.buildRecentCreate().equals(uri)) {
-            mType = TYPE_RECENT_CREATE;
-        } else {
-            mType = TYPE_NORMAL;
-        }
+        mType = getArguments().getInt(EXTRA_TYPE);
 
         mCallbacks = new LoaderCallbacks<List<Document>>() {
             @Override
@@ -140,6 +142,8 @@ public class DirectoryFragment extends Fragment {
                 final Uri contentsUri;
                 if (mType == TYPE_NORMAL) {
                     contentsUri = DocumentsContract.buildContentsUri(uri);
+                } else if (mType == TYPE_RECENT_OPEN) {
+                    contentsUri = RecentsProvider.buildRecentOpen();
                 } else {
                     contentsUri = uri;
                 }
@@ -147,8 +151,7 @@ public class DirectoryFragment extends Fragment {
                 final Predicate<Document> filter = new MimePredicate(state.acceptMimes);
 
                 final Comparator<Document> sortOrder;
-                if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN
-                        || mType == TYPE_RECENT_CREATE) {
+                if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
                     sortOrder = new Document.DateComparator();
                 } else if (state.sortOrder == DisplayState.SORT_ORDER_NAME) {
                     sortOrder = new Document.NameComparator();
@@ -170,56 +173,30 @@ public class DirectoryFragment extends Fragment {
             }
         };
 
+        updateDisplayState();
+
         return view;
     }
 
     @Override
     public void onStart() {
         super.onStart();
-        getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
+        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
     }
 
     @Override
     public void onStop() {
         super.onStop();
-        getLoaderManager().destroyLoader(LOADER_DOCUMENTS);
-    }
-
-    @Override
-    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-        super.onCreateOptionsMenu(menu, inflater);
-        inflater.inflate(R.menu.directory, menu);
+        getLoaderManager().destroyLoader(mLoaderId);
     }
 
-    @Override
-    public void onPrepareOptionsMenu(Menu menu) {
-        super.onPrepareOptionsMenu(menu);
+    public void updateDisplayState() {
         final DisplayState state = getDisplayState(this);
-        menu.findItem(R.id.menu_grid).setVisible(state.mode != DisplayState.MODE_GRID);
-        menu.findItem(R.id.menu_list).setVisible(state.mode != DisplayState.MODE_LIST);
-    }
 
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        final DisplayState state = getDisplayState(this);
-        final int id = item.getItemId();
-        if (id == R.id.menu_grid) {
-            state.mode = DisplayState.MODE_GRID;
-            updateMode();
-            getFragmentManager().invalidateOptionsMenu();
-            return true;
-        } else if (id == R.id.menu_list) {
-            state.mode = DisplayState.MODE_LIST;
-            updateMode();
-            getFragmentManager().invalidateOptionsMenu();
-            return true;
-        } else {
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    private void updateMode() {
-        final DisplayState state = getDisplayState(this);
+        // TODO: avoid kicking loader when sort didn't change
+        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
+        mListView.smoothScrollToPosition(0);
+        mGridView.smoothScrollToPosition(0);
 
         mListView.setVisibility(state.mode == DisplayState.MODE_LIST ? View.VISIBLE : View.GONE);
         mGridView.setVisibility(state.mode == DisplayState.MODE_GRID ? View.VISIBLE : View.GONE);
@@ -250,12 +227,6 @@ public class DirectoryFragment extends Fragment {
         }
     }
 
-    public void updateSortOrder() {
-        getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
-        mListView.smoothScrollToPosition(0);
-        mGridView.smoothScrollToPosition(0);
-    }
-
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
@@ -309,7 +280,7 @@ public class DirectoryFragment extends Fragment {
             if (checked) {
                 // Directories cannot be checked
                 final Document doc = mAdapter.getItem(position);
-                if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
+                if (doc.isDirectory()) {
                     mCurrentView.setItemChecked(position, false);
                 }
             }
@@ -337,10 +308,10 @@ public class DirectoryFragment extends Fragment {
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
             final Context context = parent.getContext();
+            final DisplayState state = getDisplayState(DirectoryFragment.this);
 
             if (convertView == null) {
                 final LayoutInflater inflater = LayoutInflater.from(context);
-                final DisplayState state = getDisplayState(DirectoryFragment.this);
                 if (state.mode == DisplayState.MODE_LIST) {
                     convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
                 } else if (state.mode == DisplayState.MODE_GRID) {
@@ -352,9 +323,12 @@ public class DirectoryFragment extends Fragment {
 
             final Document doc = getItem(position);
 
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
-            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+            final TextView date = (TextView) convertView.findViewById(R.id.date);
+            final TextView size = (TextView) convertView.findViewById(R.id.size);
 
             if (doc.isThumbnailSupported()) {
                 // TODO: load thumbnails async
@@ -365,8 +339,37 @@ public class DirectoryFragment extends Fragment {
             }
 
             title.setText(doc.displayName);
-            if (summary != null) {
-                summary.setText(DateUtils.getRelativeTimeSpanString(doc.lastModified));
+
+            if (mType == TYPE_NORMAL || mType == TYPE_SEARCH) {
+                icon1.setVisibility(View.GONE);
+                if (doc.summary != null) {
+                    summary.setText(doc.summary);
+                    summary.setVisibility(View.VISIBLE);
+                } else {
+                    summary.setVisibility(View.INVISIBLE);
+                }
+            } else if (mType == TYPE_RECENT_OPEN) {
+                final Root root = RootsCache.findRoot(context, doc);
+                icon1.setVisibility(View.VISIBLE);
+                icon1.setImageDrawable(root.icon);
+                summary.setText(root.getDirectoryString());
+                summary.setVisibility(View.VISIBLE);
+            }
+
+            // TODO: omit year from format
+            date.setText(DateUtils.formatSameDayTime(
+                    doc.lastModified, System.currentTimeMillis(), DateFormat.SHORT,
+                    DateFormat.SHORT));
+
+            if (state.showSize) {
+                size.setVisibility(View.VISIBLE);
+                if (doc.isDirectory()) {
+                    size.setText(null);
+                } else {
+                    size.setText(Formatter.formatFileSize(context, doc.size));
+                }
+            } else {
+                size.setVisibility(View.GONE);
             }
 
             return convertView;
index 6067581..6784d70 100644 (file)
@@ -28,7 +28,6 @@ import android.database.Cursor;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
 import android.os.Bundle;
-import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.DocumentColumns;
 import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.view.GravityCompat;
@@ -42,27 +41,23 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
 import android.widget.SearchView;
+import android.widget.SearchView.OnCloseListener;
 import android.widget.SearchView.OnQueryTextListener;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.documentsui.model.Document;
+import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.model.Root;
 
-import org.json.JSONArray;
-import org.json.JSONException;
-
 import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.List;
 
 public class DocumentsActivity extends Activity {
     public static final String TAG = "Documents";
 
-    // TODO: share backend root cache with recents provider
-
-    private static final int ACTION_OPEN = 1;
-    private static final int ACTION_CREATE = 2;
+    public static final int ACTION_OPEN = 1;
+    public static final int ACTION_CREATE = 2;
 
     private int mAction;
 
@@ -72,11 +67,12 @@ public class DocumentsActivity extends Activity {
     private DrawerLayout mDrawerLayout;
     private ActionBarDrawerToggle mDrawerToggle;
 
-    private Root mCurrentRoot;
-
     private final DisplayState mDisplayState = new DisplayState();
 
-    private LinkedList<Document> mStack = new LinkedList<Document>();
+    /** Current user navigation stack; empty implies recents. */
+    private DocumentStack mStack;
+    /** Currently active search, overriding any stack. */
+    private String mCurrentSearch;
 
     @Override
     public void onCreate(Bundle icicle) {
@@ -135,19 +131,14 @@ public class DocumentsActivity extends Activity {
                 .query(RecentsProvider.buildResume(packageName), null, null, null, null);
         try {
             if (cursor.moveToFirst()) {
-                final String rawStack = cursor.getString(
+                final String raw = cursor.getString(
                         cursor.getColumnIndex(RecentsProvider.COL_PATH));
-                restoreStack(rawStack);
+                mStack = DocumentStack.deserialize(getContentResolver(), raw);
             }
         } finally {
             cursor.close();
         }
 
-        // Start in recents if no restored stack
-        if (mStack.isEmpty()) {
-            onRootPicked(RootsCache.getRecentOpenRoot(this), false);
-        }
-
         updateDirectoryFragment();
     }
 
@@ -201,7 +192,7 @@ public class DocumentsActivity extends Activity {
             final Root root = getCurrentRoot();
             actionBar.setIcon(root != null ? root.icon : null);
 
-            if (getCurrentRoot().isRecents) {
+            if (root.isRecents) {
                 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
                 actionBar.setTitle(root.title);
             } else {
@@ -229,10 +220,8 @@ public class DocumentsActivity extends Activity {
         mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
             @Override
             public boolean onQueryTextSubmit(String query) {
-                // TODO: use second directory stack for searches?
-                final Document cwd = getCurrentDirectory();
-                final Document searchDoc = Document.fromSearch(cwd.uri, query);
-                onDocumentPicked(searchDoc);
+                mCurrentSearch = query;
+                updateDirectoryFragment();
                 mSearchView.setIconified(true);
                 return true;
             }
@@ -243,6 +232,15 @@ public class DocumentsActivity extends Activity {
             }
         });
 
+        mSearchView.setOnCloseListener(new OnCloseListener() {
+            @Override
+            public boolean onClose() {
+                mCurrentSearch = null;
+                updateDirectoryFragment();
+                return false;
+            }
+        });
+
         return true;
     }
 
@@ -265,6 +263,9 @@ public class DocumentsActivity extends Activity {
             SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
         }
 
+        menu.findItem(R.id.menu_grid).setVisible(mDisplayState.mode != DisplayState.MODE_GRID);
+        menu.findItem(R.id.menu_list).setVisible(mDisplayState.mode != DisplayState.MODE_LIST);
+
         return true;
     }
 
@@ -283,8 +284,19 @@ public class DocumentsActivity extends Activity {
             return true;
         } else if (id == R.id.menu_search) {
             return false;
+        } else if (id == R.id.menu_grid) {
+            mDisplayState.mode = DisplayState.MODE_GRID;
+            updateDisplayState();
+            invalidateOptionsMenu();
+            return true;
+        } else if (id == R.id.menu_list) {
+            mDisplayState.mode = DisplayState.MODE_LIST;
+            updateDisplayState();
+            invalidateOptionsMenu();
+            return true;
+        } else {
+            return super.onOptionsItemSelected(item);
         }
-        return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -339,7 +351,8 @@ public class DocumentsActivity extends Activity {
             if (cwd != null) {
                 title.setText(cwd.displayName);
             } else {
-                title.setText(null);
+                // No directory means recents
+                title.setText(R.string.root_recent);
             }
 
             summary.setText((String) getItem(position));
@@ -365,13 +378,18 @@ public class DocumentsActivity extends Activity {
         @Override
         public boolean onNavigationItemSelected(int itemPosition, long itemId) {
             mDisplayState.sortOrder = itemPosition;
-            DirectoryFragment.get(getFragmentManager()).updateSortOrder();
+            updateDisplayState();
             return true;
         }
     };
 
     public Root getCurrentRoot() {
-        return mCurrentRoot;
+        final Document cwd = getCurrentDirectory();
+        if (cwd != null) {
+            return RootsCache.findRoot(this, cwd);
+        } else {
+            return RootsCache.getRecentsRoot(this);
+        }
     }
 
     public Document getCurrentDirectory() {
@@ -385,19 +403,47 @@ public class DocumentsActivity extends Activity {
     private void updateDirectoryFragment() {
         final FragmentManager fm = getFragmentManager();
         final Document cwd = getCurrentDirectory();
-        if (cwd != null) {
-            DirectoryFragment.show(fm, cwd.uri);
+        if (cwd == null) {
+            // No directory means recents
+            if (mAction == ACTION_CREATE) {
+                RecentsCreateFragment.show(fm);
+            } else {
+                DirectoryFragment.showRecentsOpen(fm);
+            }
+        } else {
+            if (mCurrentSearch != null) {
+                // Ongoing search
+                DirectoryFragment.showSearch(fm, cwd.uri, mCurrentSearch);
+            } else {
+                // Normal boring directory
+                DirectoryFragment.showNormal(fm, cwd.uri);
+            }
         }
+
         updateActionBar();
         invalidateOptionsMenu();
         dumpStack();
     }
 
+    private void updateDisplayState() {
+        // TODO: handle multiple directory stacks on tablets
+        DirectoryFragment.get(getFragmentManager()).updateDisplayState();
+    }
+
+    public void onStackPicked(DocumentStack stack) {
+        mStack = stack;
+        updateDirectoryFragment();
+    }
+
     public void onRootPicked(Root root, boolean closeDrawer) {
         // Clear entire backstack and start in new root
         mStack.clear();
-        mCurrentRoot = root;
-        onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+
+        if (!root.isRecents) {
+            onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+        } else {
+            updateDirectoryFragment();
+        }
 
         if (closeDrawer) {
             mDrawerLayout.closeDrawers();
@@ -406,7 +452,7 @@ public class DocumentsActivity extends Activity {
 
     public void onDocumentPicked(Document doc) {
         final FragmentManager fm = getFragmentManager();
-        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
+        if (doc.isDirectory()) {
             mStack.push(doc);
             updateDirectoryFragment();
         } else if (mAction == ACTION_OPEN) {
@@ -443,52 +489,17 @@ public class DocumentsActivity extends Activity {
         }
     }
 
-    private String saveStack() {
-        if (mCurrentRoot.isRecents) return null;
-
-        final JSONArray stack = new JSONArray();
-        for (int i = 0; i < mStack.size(); i++) {
-            stack.put(mStack.get(i).uri);
-        }
-        return stack.toString();
-    }
-
-    private void restoreStack(String rawStack) {
-        Log.d(TAG, "restoreStack: " + rawStack);
-        mStack.clear();
-
-        if (rawStack == null) return;
-        try {
-            final JSONArray stack = new JSONArray(rawStack);
-            for (int i = 0; i < stack.length(); i++) {
-                final Uri uri = Uri.parse(stack.getString(i));
-                final Document doc = Document.fromUri(getContentResolver(), uri);
-                mStack.add(doc);
-            }
-        } catch (JSONException e) {
-            Log.w(TAG, "Failed to decode stack", e);
-        }
-
-        // TODO: handle roots that have gone missing
-        final Document cwd = getCurrentDirectory();
-        if (cwd != null) {
-            final String authority = cwd.uri.getAuthority();
-            final String rootId = DocumentsContract.getRootId(cwd.uri);
-            mCurrentRoot = RootsCache.findRoot(this, authority, rootId);
-        }
-    }
-
     private void onFinished(Uri... uris) {
         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
-        final String stack = saveStack();
+        final String rawStack = DocumentStack.serialize(mStack);
         if (mAction == ACTION_CREATE) {
             // Remember stack for last create
             values.clear();
-            values.put(RecentsProvider.COL_PATH, stack);
+            values.put(RecentsProvider.COL_PATH, rawStack);
             resolver.insert(RecentsProvider.buildRecentCreate(), values);
 
         } else if (mAction == ACTION_OPEN) {
@@ -503,7 +514,7 @@ public class DocumentsActivity extends Activity {
         // Remember location for next app launch
         final String packageName = getCallingPackage();
         values.clear();
-        values.put(RecentsProvider.COL_PATH, stack);
+        values.put(RecentsProvider.COL_PATH, rawStack);
         resolver.insert(RecentsProvider.buildResume(packageName), values);
 
         final Intent intent = new Intent();
@@ -532,6 +543,7 @@ public class DocumentsActivity extends Activity {
         public String[] acceptMimes;
         public int sortOrder = SORT_ORDER_NAME;
         public boolean allowMultiple = false;
+        public boolean showSize = false;
 
         public static final int MODE_LIST = 0;
         public static final int MODE_GRID = 1;
index 0d2f381..f945c6a 100644 (file)
@@ -16,8 +16,6 @@
 
 package com.android.documentsui;
 
-import android.provider.DocumentsContract;
-
 import com.android.documentsui.model.Document;
 import com.android.internal.util.Predicate;
 
@@ -30,7 +28,7 @@ public class MimePredicate implements Predicate<Document> {
 
     @Override
     public boolean apply(Document doc) {
-        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
+        if (doc.isDirectory()) {
             return true;
         }
         for (String filter : mFilters) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
new file mode 100644 (file)
index 0000000..2651e4c
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * 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.documentsui;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.text.TextUtils.TruncateAt;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.Root;
+import com.google.android.collect.Lists;
+
+import libcore.io.IoUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Display directories where recent creates took place.
+ */
+public class RecentsCreateFragment extends Fragment {
+
+    private ListView mListView;
+
+    private DocumentStackAdapter mAdapter;
+    private LoaderCallbacks<List<DocumentStack>> mCallbacks;
+
+    private static final int LOADER_RECENTS = 3;
+
+    public static void show(FragmentManager fm) {
+        final RecentsCreateFragment fragment = new RecentsCreateFragment();
+        final FragmentTransaction ft = fm.beginTransaction();
+        ft.replace(R.id.container_directory, fragment);
+        ft.commitAllowingStateLoss();
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        final Context context = inflater.getContext();
+
+        final View view = inflater.inflate(R.layout.fragment_directory, container, false);
+
+        mListView = (ListView) view.findViewById(R.id.list);
+        mListView.setOnItemClickListener(mItemListener);
+
+        mAdapter = new DocumentStackAdapter();
+        mListView.setAdapter(mAdapter);
+
+        mCallbacks = new LoaderCallbacks<List<DocumentStack>>() {
+            @Override
+            public Loader<List<DocumentStack>> onCreateLoader(int id, Bundle args) {
+                return new RecentsCreateLoader(context);
+            }
+
+            @Override
+            public void onLoadFinished(
+                    Loader<List<DocumentStack>> loader, List<DocumentStack> data) {
+                mAdapter.swapStacks(data);
+            }
+
+            @Override
+            public void onLoaderReset(Loader<List<DocumentStack>> loader) {
+                mAdapter.swapStacks(null);
+            }
+        };
+
+        return view;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        getLoaderManager().restartLoader(LOADER_RECENTS, getArguments(), mCallbacks);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        getLoaderManager().destroyLoader(LOADER_RECENTS);
+    }
+
+    private OnItemClickListener mItemListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            final DocumentStack stack = mAdapter.getItem(position);
+            ((DocumentsActivity) getActivity()).onStackPicked(stack);
+        }
+    };
+
+    public static class RecentsCreateLoader extends UriDerivativeLoader<List<DocumentStack>> {
+        public RecentsCreateLoader(Context context) {
+            super(context, RecentsProvider.buildRecentCreate());
+        }
+
+        @Override
+        public List<DocumentStack> loadInBackground(Uri uri, CancellationSignal signal) {
+            final ArrayList<DocumentStack> result = Lists.newArrayList();
+
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Cursor cursor = resolver.query(
+                    uri, null, null, null, RecentsProvider.COL_TIMESTAMP + " DESC", signal);
+            try {
+                while (cursor != null && cursor.moveToNext()) {
+                    final String rawStack = cursor.getString(
+                            cursor.getColumnIndex(RecentsProvider.COL_PATH));
+                    final DocumentStack stack = DocumentStack.deserialize(resolver, rawStack);
+                    result.add(stack);
+                }
+            } finally {
+                IoUtils.closeQuietly(cursor);
+            }
+
+            return result;
+        }
+    }
+
+    private class DocumentStackAdapter extends BaseAdapter {
+        private List<DocumentStack> mStacks;
+
+        public DocumentStackAdapter() {
+        }
+
+        public void swapStacks(List<DocumentStack> stacks) {
+            mStacks = stacks;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+
+            if (convertView == null) {
+                final LayoutInflater inflater = LayoutInflater.from(context);
+                convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
+            }
+
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final View line2 = convertView.findViewById(R.id.line2);
+
+            final DocumentStack stack = getItem(position);
+            final Root root = RootsCache.findRoot(context, stack.peek());
+            icon.setImageDrawable(root != null ? root.icon : null);
+
+            final StringBuilder builder = new StringBuilder();
+            final int size = stack.size();
+            for (int i = 0; i < size; i++) {
+                builder.append(stack.get(i).displayName);
+                if (i <  size - 1) {
+                    builder.append(" \u232a ");
+                }
+            }
+            title.setText(builder.toString());
+            title.setEllipsize(TruncateAt.MIDDLE);
+
+            line2.setVisibility(View.GONE);
+
+            return convertView;
+        }
+
+        @Override
+        public int getCount() {
+            return mStacks != null ? mStacks.size() : 0;
+        }
+
+        @Override
+        public DocumentStack getItem(int position) {
+            return mStacks.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return getItem(position).hashCode();
+        }
+    }
+}
index dbcb039..5268c1d 100644 (file)
@@ -129,11 +129,11 @@ public class RecentsProvider extends ContentProvider {
         switch (sMatcher.match(uri)) {
             case URI_RECENT_OPEN: {
                 return db.query(TABLE_RECENT_OPEN, projection,
-                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
             }
             case URI_RECENT_CREATE: {
                 return db.query(TABLE_RECENT_CREATE, projection,
-                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
             }
             case URI_RESUME: {
                 final String packageName = uri.getPathSegments().get(1);
index 1b56a20..b26db3b 100644 (file)
@@ -30,6 +30,7 @@ import android.provider.DocumentsContract;
 import android.util.Log;
 import android.util.Pair;
 
+import com.android.documentsui.model.Document;
 import com.android.documentsui.model.DocumentsProviderInfo;
 import com.android.documentsui.model.DocumentsProviderInfo.Icon;
 import com.android.documentsui.model.Root;
@@ -58,7 +59,7 @@ public class RootsCache {
 
     public static ArrayList<Root> sRootsList = Lists.newArrayList();
 
-    private static Root sRecentOpenRoot;
+    private static Root sRecentsRoot;
 
     /**
      * Gather roots from all known storage providers.
@@ -73,9 +74,9 @@ public class RootsCache {
 
         {
             // Create special root for recents
-            final Root root = Root.buildRecentOpen(context);
+            final Root root = Root.buildRecents(context);
             sRootsList.add(root);
-            sRecentOpenRoot = root;
+            sRecentsRoot = root;
         }
 
         // Query for other storage backends
@@ -125,13 +126,21 @@ public class RootsCache {
     }
 
     @GuardedBy("ActivityThread")
-    public static Root getRecentOpenRoot(Context context) {
+    public static Root findRoot(Context context, Document doc) {
+        final String authority = doc.uri.getAuthority();
+        final String rootId = DocumentsContract.getRootId(doc.uri);
+        return findRoot(context, authority, rootId);
+    }
+
+    @GuardedBy("ActivityThread")
+    public static Root getRecentsRoot(Context context) {
         ensureCache(context);
-        return sRecentOpenRoot;
+        return sRecentsRoot;
     }
 
     @GuardedBy("ActivityThread")
-    public static Collection<Root> getRoots() {
+    public static Collection<Root> getRoots(Context context) {
+        ensureCache(context);
         return sRootsList;
     }
 
index 3e645bc..c4e9c15 100644 (file)
@@ -69,7 +69,7 @@ public class RootsFragment extends Fragment {
         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
         mList = (ListView) view.findViewById(android.R.id.list);
 
-        mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots());
+        mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots(context));
         mList.setAdapter(mAdapter);
         mList.setOnItemClickListener(mItemListener);
 
index ed69690..3b82ba8 100644 (file)
@@ -35,26 +35,22 @@ public class Document {
     public final String displayName;
     public final long lastModified;
     public final int flags;
+    public final String summary;
+    public final long size;
 
-    private Document(Uri uri, String mimeType, String displayName, long lastModified, int flags) {
+    private Document(Uri uri, String mimeType, String displayName, long lastModified, int flags,
+            String summary, long size) {
         this.uri = uri;
         this.mimeType = mimeType;
         this.displayName = displayName;
         this.lastModified = lastModified;
         this.flags = flags;
+        this.summary = summary;
+        this.size = size;
     }
 
     public static Document fromRoot(ContentResolver resolver, Root root) {
-        if (root.isRecents) {
-            final Uri uri = root.uri;
-            final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
-            final String displayName = root.title;
-            final long lastModified = -1;
-            final int flags = 0;
-            return new Document(uri, mimeType, displayName, lastModified, flags);
-        } else {
-            return fromUri(resolver, root.uri);
-        }
+        return fromUri(resolver, root.uri);
     }
 
     public static Document fromDirectoryCursor(Uri parent, Cursor cursor) {
@@ -67,8 +63,10 @@ public class Document {
         final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
         final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
         final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
+        final String summary = getCursorString(cursor, DocumentColumns.SUMMARY);
+        final long size = getCursorLong(cursor, DocumentColumns.SIZE);
 
-        return new Document(uri, mimeType, displayName, lastModified, flags);
+        return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
     }
 
     public static Document fromRecentOpenCursor(ContentResolver resolver, Cursor cursor) {
@@ -84,8 +82,10 @@ public class Document {
             final String displayName = getCursorString(itemCursor, DocumentColumns.DISPLAY_NAME);
             final int flags = getCursorInt(itemCursor, DocumentColumns.FLAGS)
                     & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
+            final String summary = getCursorString(cursor, DocumentColumns.SUMMARY);
+            final long size = getCursorLong(cursor, DocumentColumns.SIZE);
 
-            return new Document(uri, mimeType, displayName, lastModified, flags);
+            return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
         } finally {
             itemCursor.close();
         }
@@ -101,22 +101,15 @@ public class Document {
             final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
             final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
             final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
+            final String summary = getCursorString(cursor, DocumentColumns.SUMMARY);
+            final long size = getCursorLong(cursor, DocumentColumns.SIZE);
 
-            return new Document(uri, mimeType, displayName, lastModified, flags);
+            return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
         } finally {
             cursor.close();
         }
     }
 
-    public static Document fromSearch(Uri relatedUri, String query) {
-        final Uri uri = DocumentsContract.buildSearchUri(relatedUri, query);
-        final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
-        final String displayName = query;
-        final long lastModified = System.currentTimeMillis();
-        final int flags = 0;
-        return new Document(uri, mimeType, displayName, lastModified, flags);
-    }
-
     @Override
     public String toString() {
         return "Document{name=" + displayName + ", uri=" + uri + "}";
@@ -134,23 +127,30 @@ public class Document {
         return (flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0;
     }
 
+    public boolean isDirectory() {
+        return DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType);
+    }
+
     private static String getCursorString(Cursor cursor, String columnName) {
-        return cursor.getString(cursor.getColumnIndexOrThrow(columnName));
+        final int index = cursor.getColumnIndex(columnName);
+        return (index != -1) ? cursor.getString(index) : null;
     }
 
     private static long getCursorLong(Cursor cursor, String columnName) {
-        return cursor.getLong(cursor.getColumnIndexOrThrow(columnName));
+        final int index = cursor.getColumnIndex(columnName);
+        return (index != -1) ? cursor.getLong(index) : 0;
     }
 
     private static int getCursorInt(Cursor cursor, String columnName) {
-        return cursor.getInt(cursor.getColumnIndexOrThrow(columnName));
+        final int index = cursor.getColumnIndex(columnName);
+        return (index != -1) ? cursor.getInt(index) : 0;
     }
 
     public static class NameComparator implements Comparator<Document> {
         @Override
         public int compare(Document lhs, Document rhs) {
-            final boolean leftDir = DocumentsContract.MIME_TYPE_DIRECTORY.equals(lhs.mimeType);
-            final boolean rightDir = DocumentsContract.MIME_TYPE_DIRECTORY.equals(rhs.mimeType);
+            final boolean leftDir = lhs.isDirectory();
+            final boolean rightDir = rhs.isDirectory();
 
             if (leftDir != rightDir) {
                 return leftDir ? -1 : 1;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
new file mode 100644 (file)
index 0000000..dade8a3
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.documentsui.model;
+
+import static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.LinkedList;
+
+/**
+ * Representation of a stack of {@link Document}, usually the result of a
+ * user-driven traversal.
+ */
+public class DocumentStack extends LinkedList<Document> {
+
+    public static String serialize(DocumentStack stack) {
+        final JSONArray json = new JSONArray();
+        for (int i = 0; i < stack.size(); i++) {
+            json.put(stack.get(i).uri);
+        }
+        return json.toString();
+    }
+
+    public static DocumentStack deserialize(ContentResolver resolver, String raw) {
+        Log.d(TAG, "restoreStack: " + raw);
+
+        final DocumentStack stack = new DocumentStack();
+        try {
+            final JSONArray json = new JSONArray(raw);
+            for (int i = 0; i < json.length(); i++) {
+                final Uri uri = Uri.parse(json.getString(i));
+                final Document doc = Document.fromUri(resolver, uri);
+                stack.add(doc);
+            }
+        } catch (JSONException e) {
+            Log.w(TAG, "Failed to decode stack", e);
+        }
+
+        // TODO: handle roots that have gone missing
+        return stack;
+    }
+}
index 9d816d7..629dbc4 100644 (file)
@@ -43,12 +43,12 @@ public class Root {
     public String summary;
     public boolean isRecents;
 
-    public static Root buildRecentOpen(Context context) {
+    public static Root buildRecents(Context context) {
         final PackageManager pm = context.getPackageManager();
         final Root root = new Root();
         root.rootId = null;
         root.rootType = DocumentsContract.ROOT_TYPE_SHORTCUT;
-        root.uri = RecentsProvider.buildRecentOpen();
+        root.uri = null;
         root.icon = context.getResources().getDrawable(R.drawable.ic_dir);
         root.title = context.getString(R.string.root_recent);
         root.summary = null;
@@ -92,6 +92,13 @@ public class Root {
         return root;
     }
 
+    /**
+     * Return string most suited to showing in a directory listing.
+     */
+    public String getDirectoryString() {
+        return (summary != null) ? summary : title;
+    }
+
     public static class RootComparator implements Comparator<Root> {
         @Override
         public int compare(Root lhs, Root rhs) {