<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"
<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" />
<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>
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>
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>
+++ /dev/null
-<?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>
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;
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;
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();
}
@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();
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
final Uri contentsUri;
if (mType == TYPE_NORMAL) {
contentsUri = DocumentsContract.buildContentsUri(uri);
+ } else if (mType == TYPE_RECENT_OPEN) {
+ contentsUri = RecentsProvider.buildRecentOpen();
} else {
contentsUri = uri;
}
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();
}
};
+ 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);
}
}
- 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) {
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);
}
}
@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) {
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
}
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;
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;
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;
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) {
.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();
}
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 {
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;
}
}
});
+ mSearchView.setOnCloseListener(new OnCloseListener() {
+ @Override
+ public boolean onClose() {
+ mCurrentSearch = null;
+ updateDirectoryFragment();
+ return false;
+ }
+ });
+
return true;
}
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;
}
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
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));
@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() {
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();
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) {
}
}
- 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) {
// 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();
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;
package com.android.documentsui;
-import android.provider.DocumentsContract;
-
import com.android.documentsui.model.Document;
import com.android.internal.util.Predicate;
@Override
public boolean apply(Document doc) {
- if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
+ if (doc.isDirectory()) {
return true;
}
for (String filter : mFilters) {
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
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);
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;
public static ArrayList<Root> sRootsList = Lists.newArrayList();
- private static Root sRecentOpenRoot;
+ private static Root sRecentsRoot;
/**
* Gather roots from all known storage providers.
{
// 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
}
@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;
}
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);
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) {
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) {
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();
}
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 + "}";
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;
--- /dev/null
+/*
+ * 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;
+ }
+}
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;
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) {