diff options
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); + } } |