summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt1
-rw-r--r--core/java/android/provider/DocumentsContract.java32
-rw-r--r--packages/DocumentsUI/res/layout/activity.xml8
-rw-r--r--packages/DocumentsUI/res/layout/fragment_roots.xml20
-rw-r--r--packages/DocumentsUI/res/layout/item_root_header.xml29
-rw-r--r--packages/DocumentsUI/res/values/strings.xml4
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java90
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java229
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsCache.java167
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java186
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java4
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java160
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Document.java2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Root.java20
15 files changed, 734 insertions, 224 deletions
diff --git a/api/current.txt b/api/current.txt
index 5fd52e330ae4..2b05c3ca6844 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20301,6 +20301,7 @@ package android.provider {
field public static final java.lang.String FLAGS = "flags";
field public static final java.lang.String LAST_MODIFIED = "last_modified";
field public static final java.lang.String MIME_TYPE = "mime_type";
+ field public static final java.lang.String SUMMARY = "summary";
}
public static abstract interface DocumentsContract.RootColumns {
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 9c2bb49ffbbd..289531e52a27 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -267,11 +267,43 @@ public final class DocumentsContract {
* Type: INTEGER (int)
*/
public static final String FLAGS = "flags";
+
+ /**
+ * Summary for this document, or {@code null} to omit.
+ * <p>
+ * Type: STRING
+ */
+ public static final String SUMMARY = "summary";
}
+ /**
+ * Root that represents a cloud-based storage service.
+ *
+ * @see RootColumns#ROOT_TYPE
+ */
public static final int ROOT_TYPE_SERVICE = 1;
+
+ /**
+ * Root that represents a shortcut to content that may be available
+ * elsewhere through another storage root.
+ *
+ * @see RootColumns#ROOT_TYPE
+ */
public static final int ROOT_TYPE_SHORTCUT = 2;
+
+ /**
+ * Root that represents a physical storage device.
+ *
+ * @see RootColumns#ROOT_TYPE
+ */
public static final int ROOT_TYPE_DEVICE = 3;
+
+ /**
+ * Root that represents a physical storage device that should only be
+ * displayed to advanced users.
+ *
+ * @see RootColumns#ROOT_TYPE
+ */
public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
/**
diff --git a/packages/DocumentsUI/res/layout/activity.xml b/packages/DocumentsUI/res/layout/activity.xml
index d4a01d3f5b52..ff28e41c5908 100644
--- a/packages/DocumentsUI/res/layout/activity.xml
+++ b/packages/DocumentsUI/res/layout/activity.xml
@@ -25,20 +25,20 @@
android:orientation="vertical">
<FrameLayout
- android:id="@+id/directory"
+ android:id="@+id/container_directory"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
<FrameLayout
- android:id="@+id/save"
+ android:id="@+id/container_save"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
- <ListView
- android:id="@+id/roots_list"
+ <FrameLayout
+ android:id="@+id/container_roots"
android:layout_width="250dp"
android:layout_height="match_parent"
android:layout_gravity="start"
diff --git a/packages/DocumentsUI/res/layout/fragment_roots.xml b/packages/DocumentsUI/res/layout/fragment_roots.xml
new file mode 100644
index 000000000000..d77289216097
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/fragment_roots.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<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" />
diff --git a/packages/DocumentsUI/res/layout/item_root_header.xml b/packages/DocumentsUI/res/layout/item_root_header.xml
new file mode 100644
index 000000000000..2b9a46f32df4
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/item_root_header.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAllCaps="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAlignment="viewStart" />
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 3eda207712fd..2ff5d0309b63 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -41,4 +41,8 @@
<string name="root_recent">Recent</string>
+ <string name="root_type_service">Services</string>
+ <string name="root_type_shortcut">Shortcuts</string>
+ <string name="root_type_device">Devices</string>
+
</resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
new file mode 100644
index 000000000000..e19505f4b05a
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -0,0 +1,90 @@
+/*
+ * 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.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.android.documentsui.model.Document;
+
+/**
+ * Dialog to create a new directory.
+ */
+public class CreateDirectoryFragment extends DialogFragment {
+ private static final String TAG_CREATE_DIRECTORY = "create_directory";
+
+ public static void show(FragmentManager fm) {
+ final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
+ dialog.show(fm, TAG_CREATE_DIRECTORY);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity();
+ final ContentResolver resolver = context.getContentResolver();
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+ final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
+ final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
+
+ builder.setTitle(R.string.menu_create_dir);
+ builder.setView(view);
+
+ builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String displayName = text1.getText().toString();
+
+ final ContentValues values = new ContentValues();
+ values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
+ values.put(DocumentColumns.DISPLAY_NAME, displayName);
+
+ final DocumentsActivity activity = (DocumentsActivity) getActivity();
+ final Document cwd = activity.getCurrentDirectory();
+
+ final Uri childUri = resolver.insert(cwd.uri, values);
+ if (childUri != null) {
+ // Navigate into newly created child
+ final Document childDoc = Document.fromUri(resolver, childUri);
+ activity.onDocumentPicked(childDoc);
+ } else {
+ Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+
+ return builder.create();
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index f6f3f9c208b1..1443f2679efb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -87,13 +87,13 @@ public class DirectoryFragment extends Fragment {
fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
- ft.replace(R.id.directory, fragment);
+ ft.replace(R.id.container_directory, fragment);
ft.commitAllowingStateLoss();
}
public static DirectoryFragment get(FragmentManager fm) {
// TODO: deal with multiple directories shown at once
- return (DirectoryFragment) fm.findFragmentById(R.id.directory);
+ return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
}
@Override
@@ -360,7 +360,7 @@ public class DirectoryFragment extends Fragment {
// TODO: load thumbnails async
icon.setImageURI(doc.uri);
} else {
- icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
+ icon.setImageDrawable(RootsCache.resolveDocumentIcon(
context, doc.uri.getAuthority(), doc.mimeType));
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 0cbd1cb477bd..6067581869d1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -19,25 +19,13 @@ package com.android.documentsui;
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.ClipData;
-import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ProviderInfo;
-import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
@@ -47,37 +35,24 @@ import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.DrawerLayout.DrawerListener;
import android.util.Log;
-import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.ListView;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
import android.widget.TextView;
import android.widget.Toast;
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;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
import org.json.JSONArray;
import org.json.JSONException;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -86,8 +61,6 @@ public class DocumentsActivity extends Activity {
// TODO: share backend root cache with recents provider
- private static final String TAG_CREATE_DIRECTORY = "create_directory";
-
private static final int ACTION_OPEN = 1;
private static final int ACTION_CREATE = 2;
@@ -95,24 +68,12 @@ public class DocumentsActivity extends Activity {
private SearchView mSearchView;
+ private View mRootsContainer;
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private Root mCurrentRoot;
- /** Map from authority to cached info */
- private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
- /** Map from (authority+rootId) to cached info */
- private static HashMap<Pair<String, String>, Root> sRoots = Maps.newHashMap();
-
- // TODO: remove once adapter split by type
- private static ArrayList<Root> sRootsList = Lists.newArrayList();
-
- private static Root sRecentOpenRoot;
-
- private RootsAdapter mRootsAdapter;
- private ListView mRootsList;
-
private final DisplayState mDisplayState = new DisplayState();
private LinkedList<Document> mStack = new LinkedList<Document>();
@@ -153,11 +114,11 @@ public class DocumentsActivity extends Activity {
SaveFragment.show(getFragmentManager(), mimeType, title);
}
+ RootsFragment.show(getFragmentManager());
+
+ mRootsContainer = findViewById(R.id.container_roots);
+
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
- mRootsAdapter = new RootsAdapter(this, sRootsList);
- mRootsList = (ListView) findViewById(R.id.roots_list);
- mRootsList.setAdapter(mRootsAdapter);
- mRootsList.setOnItemClickListener(mRootsListener);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
@@ -165,9 +126,7 @@ public class DocumentsActivity extends Activity {
mDrawerLayout.setDrawerListener(mDrawerListener);
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
- mDrawerLayout.openDrawer(mRootsList);
-
- updateRoots();
+ mDrawerLayout.openDrawer(mRootsContainer);
// Restore last stack for calling package
// TODO: move into async loader
@@ -186,7 +145,7 @@ public class DocumentsActivity extends Activity {
// Start in recents if no restored stack
if (mStack.isEmpty()) {
- onRootPicked(sRecentOpenRoot);
+ onRootPicked(RootsCache.getRecentOpenRoot(this), false);
}
updateDirectoryFragment();
@@ -228,7 +187,7 @@ public class DocumentsActivity extends Activity {
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
- if (mDrawerLayout.isDrawerOpen(mRootsList)) {
+ if (mDrawerLayout.isDrawerOpen(mRootsContainer)) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setIcon(new ColorDrawable());
@@ -334,7 +293,7 @@ public class DocumentsActivity extends Activity {
if (size > 1) {
mStack.pop();
updateDirectoryFragment();
- } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsList)) {
+ } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsContainer)) {
// TODO: open root drawer once we can capture back key
super.onBackPressed();
} else {
@@ -434,11 +393,15 @@ public class DocumentsActivity extends Activity {
dumpStack();
}
- public void onRootPicked(Root root) {
+ 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 (closeDrawer) {
+ mDrawerLayout.closeDrawers();
+ }
}
public void onDocumentPicked(Document doc) {
@@ -511,7 +474,7 @@ public class DocumentsActivity extends Activity {
if (cwd != null) {
final String authority = cwd.uri.getAuthority();
final String rootId = DocumentsContract.getRootId(cwd.uri);
- mCurrentRoot = sRoots.get(Pair.create(authority, rootId));
+ mCurrentRoot = RootsCache.findRoot(this, authority, rootId);
}
}
@@ -577,172 +540,10 @@ public class DocumentsActivity extends Activity {
public static final int SORT_ORDER_DATE = 1;
}
- public static Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
- // Custom icons take precedence
- final DocumentsProviderInfo info = sProviders.get(authority);
- if (info != null) {
- for (Icon icon : info.customIcons) {
- if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
- return icon.icon;
- }
- }
- }
-
- if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
- return context.getResources().getDrawable(R.drawable.ic_dir);
- } else {
- final PackageManager pm = context.getPackageManager();
- final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setType(mimeType);
-
- final ResolveInfo activityInfo = pm.resolveActivity(
- intent, PackageManager.MATCH_DEFAULT_ONLY);
- if (activityInfo != null) {
- return activityInfo.loadIcon(pm);
- } else {
- return null;
- }
- }
- }
-
- /**
- * Gather roots from all known storage providers.
- */
- private void updateRoots() {
- sProviders.clear();
- sRoots.clear();
- sRootsList.clear();
-
- final Context context = this;
- final PackageManager pm = getPackageManager();
-
- // Create special roots, like recents
- {
- final Root root = Root.buildRecentOpen(context);
- sRootsList.add(root);
- sRecentOpenRoot = root;
- }
-
- // Query for other storage backends
- final List<ProviderInfo> providers = pm.queryContentProviders(
- null, -1, PackageManager.GET_META_DATA);
- for (ProviderInfo providerInfo : providers) {
- if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
- DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
- final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
- this, providerInfo);
- if (info == null) {
- Log.w(TAG, "Missing info for " + providerInfo);
- continue;
- }
-
- sProviders.put(info.providerInfo.authority, info);
-
- // TODO: remove deprecated customRoots flag
- // TODO: populate roots on background thread, and cache results
- final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
- final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
- try {
- while (cursor.moveToNext()) {
- final Root root = Root.fromCursor(this, info, cursor);
- sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
- sRootsList.add(root);
- }
- } finally {
- cursor.close();
- }
- }
- }
- }
-
- private OnItemClickListener mRootsListener = new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final Root root = mRootsAdapter.getItem(position);
- onRootPicked(root);
- mDrawerLayout.closeDrawers();
- }
- };
-
private void dumpStack() {
Log.d(TAG, "Current stack:");
for (Document doc : mStack) {
Log.d(TAG, "--> " + doc);
}
}
-
- public static class RootsAdapter extends ArrayAdapter<Root> {
- public RootsAdapter(Context context, List<Root> list) {
- super(context, android.R.layout.simple_list_item_1, list);
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.item_root, parent, false);
- }
-
- final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
- final TextView title = (TextView) convertView.findViewById(android.R.id.title);
- final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
-
- final Root root = getItem(position);
- icon.setImageDrawable(root.icon);
- title.setText(root.title);
-
- summary.setText(root.summary);
- summary.setVisibility(root.summary != null ? View.VISIBLE : View.GONE);
-
- return convertView;
- }
- }
-
- public static class CreateDirectoryFragment extends DialogFragment {
- public static void show(FragmentManager fm) {
- final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
- dialog.show(fm, TAG_CREATE_DIRECTORY);
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Context context = getActivity();
- final ContentResolver resolver = context.getContentResolver();
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(context);
- final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
-
- final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
- final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
-
- builder.setTitle(R.string.menu_create_dir);
- builder.setView(view);
-
- builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- final String displayName = text1.getText().toString();
-
- final ContentValues values = new ContentValues();
- values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
- values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
- final DocumentsActivity activity = (DocumentsActivity) getActivity();
- final Document cwd = activity.getCurrentDirectory();
-
- final Uri childUri = resolver.insert(cwd.uri, values);
- if (childUri != null) {
- // Navigate into newly created child
- final Document childDoc = Document.fromUri(resolver, childUri);
- activity.onDocumentPicked(childDoc);
- } else {
- Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
- }
- }
- });
- builder.setNegativeButton(android.R.string.cancel, null);
-
- return builder.create();
- }
- }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
new file mode 100644
index 000000000000..1b56a2003157
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -0,0 +1,167 @@
+/*
+ * 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 static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.documentsui.model.DocumentsProviderInfo;
+import com.android.documentsui.model.DocumentsProviderInfo.Icon;
+import com.android.documentsui.model.Root;
+import com.android.internal.annotations.GuardedBy;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Cache of known storage backends and their roots.
+ */
+public class RootsCache {
+
+ // TODO: cache roots in local provider to avoid spinning up backends
+
+ private static boolean sCached = false;
+
+ /** Map from authority to cached info */
+ private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
+ /** Map from (authority+rootId) to cached info */
+ private static HashMap<Pair<String, String>, Root> sRoots = Maps.newHashMap();
+
+ public static ArrayList<Root> sRootsList = Lists.newArrayList();
+
+ private static Root sRecentOpenRoot;
+
+ /**
+ * Gather roots from all known storage providers.
+ */
+ private static void ensureCache(Context context) {
+ if (sCached) return;
+ sCached = true;
+
+ sProviders.clear();
+ sRoots.clear();
+ sRootsList.clear();
+
+ {
+ // Create special root for recents
+ final Root root = Root.buildRecentOpen(context);
+ sRootsList.add(root);
+ sRecentOpenRoot = root;
+ }
+
+ // Query for other storage backends
+ final PackageManager pm = context.getPackageManager();
+ final List<ProviderInfo> providers = pm.queryContentProviders(
+ null, -1, PackageManager.GET_META_DATA);
+ for (ProviderInfo providerInfo : providers) {
+ if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
+ DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
+ final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
+ context, providerInfo);
+ if (info == null) {
+ Log.w(TAG, "Missing info for " + providerInfo);
+ continue;
+ }
+
+ sProviders.put(info.providerInfo.authority, info);
+
+ // TODO: remove deprecated customRoots flag
+ // TODO: populate roots on background thread, and cache results
+ final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
+ final Cursor cursor = context.getContentResolver()
+ .query(uri, null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ final Root root = Root.fromCursor(context, info, cursor);
+ sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
+ sRootsList.add(root);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ @GuardedBy("ActivityThread")
+ public static DocumentsProviderInfo findProvider(Context context, String authority) {
+ ensureCache(context);
+ return sProviders.get(authority);
+ }
+
+ @GuardedBy("ActivityThread")
+ public static Root findRoot(Context context, String authority, String rootId) {
+ ensureCache(context);
+ return sRoots.get(Pair.create(authority, rootId));
+ }
+
+ @GuardedBy("ActivityThread")
+ public static Root getRecentOpenRoot(Context context) {
+ ensureCache(context);
+ return sRecentOpenRoot;
+ }
+
+ @GuardedBy("ActivityThread")
+ public static Collection<Root> getRoots() {
+ return sRootsList;
+ }
+
+ @GuardedBy("ActivityThread")
+ public static Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
+ // Custom icons take precedence
+ ensureCache(context);
+ final DocumentsProviderInfo info = sProviders.get(authority);
+ if (info != null) {
+ for (Icon icon : info.customIcons) {
+ if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
+ return icon.icon;
+ }
+ }
+ }
+
+ if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+ return context.getResources().getDrawable(R.drawable.ic_dir);
+ } else {
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setType(mimeType);
+
+ final ResolveInfo activityInfo = pm.resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (activityInfo != null) {
+ return activityInfo.loadIcon(pm);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
new file mode 100644
index 000000000000..3e645bcb6241
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -0,0 +1,186 @@
+/*
+ * 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 static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.documentsui.SectionedListAdapter.SectionAdapter;
+import com.android.documentsui.model.Root;
+import com.android.documentsui.model.Root.RootComparator;
+
+import java.util.Collection;
+
+/**
+ * Display list of known storage backend roots.
+ */
+public class RootsFragment extends Fragment {
+
+ private ListView mList;
+ private SectionedRootsAdapter mAdapter;
+
+ public static void show(FragmentManager fm) {
+ final RootsFragment fragment = new RootsFragment();
+
+ final FragmentTransaction ft = fm.beginTransaction();
+ ft.replace(R.id.container_roots, fragment);
+ ft.commitAllowingStateLoss();
+ }
+
+ public static RootsFragment get(FragmentManager fm) {
+ return (RootsFragment) fm.findFragmentById(R.id.container_roots);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final Context context = inflater.getContext();
+
+ 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());
+ mList.setAdapter(mAdapter);
+ mList.setOnItemClickListener(mItemListener);
+
+ return view;
+ }
+
+ private OnItemClickListener mItemListener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final Root root = (Root) mAdapter.getItem(position);
+ ((DocumentsActivity) getActivity()).onRootPicked(root, true);
+ }
+ };
+
+ public static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+ private int mHeaderId;
+
+ public RootsAdapter(Context context, int headerId) {
+ super(context, 0);
+ mHeaderId = headerId;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_root, parent, false);
+ }
+
+ final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+ final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+ final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+
+ final Root root = getItem(position);
+ icon.setImageDrawable(root.icon);
+ title.setText(root.title);
+
+ summary.setText(root.summary);
+ summary.setVisibility(root.summary != null ? View.VISIBLE : View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public View getHeaderView(View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_root_header, parent, false);
+ }
+
+ final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+ title.setText(mHeaderId);
+
+ return convertView;
+ }
+ }
+
+ public static class SectionedRootsAdapter extends SectionedListAdapter {
+ private final RootsAdapter mServices;
+ private final RootsAdapter mShortcuts;
+ private final RootsAdapter mDevices;
+ private final RootsAdapter mDevicesAdvanced;
+
+ public SectionedRootsAdapter(Context context, Collection<Root> roots) {
+ mServices = new RootsAdapter(context, R.string.root_type_service);
+ mShortcuts = new RootsAdapter(context, R.string.root_type_shortcut);
+ mDevices = new RootsAdapter(context, R.string.root_type_device);
+ mDevicesAdvanced = new RootsAdapter(context, R.string.root_type_device);
+
+ for (Root root : roots) {
+ Log.d(TAG, "Found rootType=" + root.rootType);
+ switch (root.rootType) {
+ case DocumentsContract.ROOT_TYPE_SERVICE:
+ mServices.add(root);
+ break;
+ case DocumentsContract.ROOT_TYPE_SHORTCUT:
+ mShortcuts.add(root);
+ break;
+ case DocumentsContract.ROOT_TYPE_DEVICE:
+ mDevices.add(root);
+ mDevicesAdvanced.add(root);
+ break;
+ case DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED:
+ mDevicesAdvanced.add(root);
+ break;
+ }
+ }
+
+ final RootComparator comp = new RootComparator();
+ mServices.sort(comp);
+ mShortcuts.sort(comp);
+ mDevices.sort(comp);
+ mDevicesAdvanced.sort(comp);
+
+ // TODO: switch to hide advanced items by default
+ setShowAdvanced(true);
+ }
+
+ public void setShowAdvanced(boolean showAdvanced) {
+ clearSections();
+ if (mServices.getCount() > 0) {
+ addSection(mServices);
+ }
+ if (mShortcuts.getCount() > 0) {
+ addSection(mShortcuts);
+ }
+
+ final RootsAdapter devices = showAdvanced ? mDevicesAdvanced : mDevices;
+ if (devices.getCount() > 0) {
+ addSection(devices);
+ }
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index cdc399d00a6a..304f6e3a7ed8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -49,7 +49,7 @@ public class SaveFragment extends Fragment {
fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
- ft.replace(R.id.save, fragment, TAG);
+ ft.replace(R.id.container_save, fragment, TAG);
ft.commitAllowingStateLoss();
}
@@ -65,7 +65,7 @@ public class SaveFragment extends Fragment {
final View view = inflater.inflate(R.layout.fragment_save, container, false);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
- icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
+ icon.setImageDrawable(RootsCache.resolveDocumentIcon(
context, null, getArguments().getString(EXTRA_MIME_TYPE)));
mDisplayName = (EditText) view.findViewById(android.R.id.title);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java b/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java
new file mode 100644
index 000000000000..aacce65cd02e
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * 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.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Adapter that combines multiple adapters as sections, asking each section to
+ * provide a header, and correctly handling item types across child adapters.
+ */
+public class SectionedListAdapter extends BaseAdapter {
+ private ArrayList<SectionAdapter> mSections = Lists.newArrayList();
+
+ public interface SectionAdapter extends ListAdapter {
+ public View getHeaderView(View convertView, ViewGroup parent);
+ }
+
+ public void clearSections() {
+ mSections.clear();
+ notifyDataSetChanged();
+ }
+
+ public void addSection(SectionAdapter adapter) {
+ mSections.add(adapter);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ int count = 0;
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ count += mSections.get(i).getCount() + 1;
+ }
+ return count;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ final SectionAdapter section = mSections.get(i);
+ final int sectionSize = section.getCount() + 1;
+
+ // Check if position inside this section
+ if (position == 0) {
+ return section;
+ } else if (position < sectionSize) {
+ return section.getItem(position - 1);
+ }
+
+ // Otherwise jump into next section
+ position -= sectionSize;
+ }
+ throw new IllegalStateException("Unknown position " + position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ final SectionAdapter section = mSections.get(i);
+ final int sectionSize = section.getCount() + 1;
+
+ // Check if position inside this section
+ if (position == 0) {
+ return section.getHeaderView(convertView, parent);
+ } else if (position < sectionSize) {
+ return section.getView(position - 1, convertView, parent);
+ }
+
+ // Otherwise jump into next section
+ position -= sectionSize;
+ }
+ throw new IllegalStateException("Unknown position " + position);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ final SectionAdapter section = mSections.get(i);
+ final int sectionSize = section.getCount() + 1;
+
+ // Check if position inside this section
+ if (position == 0) {
+ return false;
+ } else if (position < sectionSize) {
+ return section.isEnabled(position);
+ }
+
+ // Otherwise jump into next section
+ position -= sectionSize;
+ }
+ throw new IllegalStateException("Unknown position " + position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int type = 1;
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ final SectionAdapter section = mSections.get(i);
+ final int sectionSize = section.getCount() + 1;
+
+ // Check if position inside this section
+ if (position == 0) {
+ return 0;
+ } else if (position < sectionSize) {
+ return type + section.getItemViewType(position - 1);
+ }
+
+ // Otherwise jump into next section
+ position -= sectionSize;
+ type += section.getViewTypeCount();
+ }
+ throw new IllegalStateException("Unknown position " + position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ int count = 1;
+ final int size = mSections.size();
+ for (int i = 0; i < size; i++) {
+ count += mSections.get(i).getViewTypeCount();
+ }
+ return count;
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
index 94b9093968a2..ed69690010f3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
@@ -155,7 +155,7 @@ public class Document {
if (leftDir != rightDir) {
return leftDir ? -1 : 1;
} else {
- return lhs.displayName.compareToIgnoreCase(rhs.displayName);
+ return Root.compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
}
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
index ef3b8d7149cc..9d816d7ace79 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
@@ -29,6 +29,8 @@ import android.provider.DocumentsContract.RootColumns;
import com.android.documentsui.R;
import com.android.documentsui.RecentsProvider;
+import java.util.Comparator;
+
/**
* Representation of a root under a storage backend.
*/
@@ -89,4 +91,22 @@ public class Root {
return root;
}
+
+ public static class RootComparator implements Comparator<Root> {
+ @Override
+ public int compare(Root lhs, Root rhs) {
+ final int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
+ if (score != 0) {
+ return score;
+ } else {
+ return compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
+ }
+ }
+ }
+
+ public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
+ if (lhs == null) return -1;
+ if (rhs == null) return 1;
+ return lhs.compareToIgnoreCase(rhs);
+ }
}