diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index e10ead9cddaf9..c26f6d47c914d 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -25,7 +25,6 @@ import android.graphics.BitmapFactory; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; -import android.os.SystemClock; import android.util.Log; import libcore.io.IoUtils; @@ -52,6 +51,9 @@ public final class DocumentsContract { */ public static final String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc"; + /** {@hide} */ + public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER"; + /** * {@link DocumentColumns#GUID} value representing the root directory of a * storage backend. diff --git a/packages/DocumentsUI/Android.mk b/packages/DocumentsUI/Android.mk new file mode 100644 index 0000000000000..1e458070591cb --- /dev/null +++ b/packages/DocumentsUI/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := DocumentsUI +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml new file mode 100644 index 0000000000000..84c5474a259f9 --- /dev/null +++ b/packages/DocumentsUI/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml new file mode 100644 index 0000000000000..89f64964a0deb --- /dev/null +++ b/packages/DocumentsUI/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + Documents + diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java new file mode 100644 index 0000000000000..d43abded6ead8 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -0,0 +1,168 @@ +/* + * 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.FragmentManager; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +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.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +public class DirectoryFragment extends ListFragment { + private DocumentsAdapter mAdapter; + private LoaderCallbacks mCallbacks; + + private static final String EXTRA_URI = "uri"; + + private static final int LOADER_DOCUMENTS = 2; + + public static void show(FragmentManager fm, Uri uri, CharSequence title) { + final Bundle args = new Bundle(); + args.putParcelable(EXTRA_URI, uri); + + final DirectoryFragment fragment = new DirectoryFragment(); + fragment.setArguments(args); + + final FragmentTransaction ft = fm.beginTransaction(); + ft.replace(android.R.id.content, fragment); + ft.addToBackStack(title.toString()); + ft.setBreadCrumbTitle(title); + ft.commitAllowingStateLoss(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Context context = inflater.getContext(); + + mAdapter = new DocumentsAdapter(context); + setListAdapter(mAdapter); + + mCallbacks = new LoaderCallbacks() { + @Override + public Loader onCreateLoader(int id, Bundle args) { + final Uri uri = args.getParcelable(EXTRA_URI); + return new CursorLoader(context, uri, null, null, null, null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + }; + + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks); + } + + @Override + public void onStop() { + super.onStop(); + getLoaderManager().destroyLoader(LOADER_DOCUMENTS); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final Cursor cursor = (Cursor) mAdapter.getItem(position); + final String guid = getCursorString(cursor, DocumentColumns.GUID); + final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE); + + final Uri uri = getArguments().getParcelable(EXTRA_URI); + final Uri childUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), guid); + + if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) { + // Nested directory picked, recurse using new fragment + final Uri childContentsUri = DocumentsContract.buildContentsUri(childUri); + final String displayName = cursor.getString( + cursor.getColumnIndex(DocumentColumns.DISPLAY_NAME)); + DirectoryFragment.show(getFragmentManager(), childContentsUri, displayName); + } else { + // Explicit file picked, return + ((DocumentsActivity) getActivity()).onDocumentPicked(childUri); + } + } + + private class DocumentsAdapter extends CursorAdapter { + public DocumentsAdapter(Context context) { + super(context, null, false); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context) + .inflate(com.android.internal.R.layout.preference, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final TextView title = (TextView) view.findViewById(android.R.id.title); + final TextView summary = (TextView) view.findViewById(android.R.id.summary); + final ImageView icon = (ImageView) view.findViewById(android.R.id.icon); + + icon.setMaxWidth(128); + icon.setMaxHeight(128); + + final String guid = getCursorString(cursor, DocumentColumns.GUID); + final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME); + final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE); + final int flags = getCursorInt(cursor, DocumentColumns.FLAGS); + + if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) { + final Uri uri = getArguments().getParcelable(EXTRA_URI); + final Uri childUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), guid); + icon.setImageURI(childUri); + } else { + icon.setImageURI(null); + } + + title.setText(displayName); + summary.setText(mimeType); + } + } + + private static String getCursorString(Cursor cursor, String columnName) { + return cursor.getString(cursor.getColumnIndex(columnName)); + } + + private static int getCursorInt(Cursor cursor, String columnName) { + return cursor.getInt(cursor.getColumnIndex(columnName)); + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java new file mode 100644 index 0000000000000..196776b7ae5e5 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java @@ -0,0 +1,115 @@ +/* + * 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.Activity; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.net.Uri; +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.ArrayAdapter; +import android.widget.ListView; + +import com.google.android.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +public class DocumentsActivity extends Activity { + private static final String TAG = "Documents"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + SourceFragment.show(getFragmentManager()); + setResult(Activity.RESULT_CANCELED); + } + + public void onDocumentPicked(Uri uri) { + Log.d(TAG, "onDocumentPicked() " + uri); + + final Intent intent = new Intent(); + intent.setData(uri); + + intent.addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION); + if (Intent.ACTION_CREATE_DOCUMENT.equals(getIntent().getAction())) { + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + setResult(Activity.RESULT_OK, intent); + finish(); + } + + public static class SourceFragment extends ListFragment { + private ArrayList mProviders = Lists.newArrayList(); + private ArrayAdapter mAdapter; + + public static void show(FragmentManager fm) { + final SourceFragment fragment = new SourceFragment(); + + final FragmentTransaction ft = fm.beginTransaction(); + ft.replace(android.R.id.content, fragment); + ft.setBreadCrumbTitle("TOP"); + ft.commitAllowingStateLoss(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Context context = inflater.getContext(); + + // Gather known storage providers + mProviders.clear(); + final List providers = context.getPackageManager() + .queryContentProviders(null, -1, PackageManager.GET_META_DATA); + for (ProviderInfo info : providers) { + if (info.metaData != null + && info.metaData.containsKey( + DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) { + mProviders.add(info); + } + } + + mAdapter = new ArrayAdapter( + context, android.R.layout.simple_list_item_1, mProviders); + setListAdapter(mAdapter); + + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final ProviderInfo info = mAdapter.getItem(position); + final Uri uri = DocumentsContract.buildContentsUri(DocumentsContract.buildDocumentUri( + info.authority, DocumentsContract.ROOT_GUID)); + final String displayName = info.name; + DirectoryFragment.show(getFragmentManager(), uri, displayName); + } + } +} diff --git a/packages/ExternalStorageProvider/Android.mk b/packages/ExternalStorageProvider/Android.mk new file mode 100644 index 0000000000000..32752b8f33a97 --- /dev/null +++ b/packages/ExternalStorageProvider/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := ExternalStorageProvider +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml new file mode 100644 index 0000000000000..37dc5b101e4bd --- /dev/null +++ b/packages/ExternalStorageProvider/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/packages/ExternalStorageProvider/res/values/strings.xml b/packages/ExternalStorageProvider/res/values/strings.xml new file mode 100644 index 0000000000000..4374cfc6e7b49 --- /dev/null +++ b/packages/ExternalStorageProvider/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + External Storage + diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java new file mode 100644 index 0000000000000..f75e3bd9b3e84 --- /dev/null +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -0,0 +1,204 @@ +/* + * 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.externalstorage; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.DocumentColumns; +import android.webkit.MimeTypeMap; + +import com.android.internal.annotations.GuardedBy; +import com.google.android.collect.Lists; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +public class ExternalStorageProvider extends ContentProvider { + private static final String TAG = "ExternalStorage"; + + private static final String AUTHORITY = "com.android.externalstorage"; + + // TODO: support searching + // TODO: support multiple storage devices + // TODO: persist GUIDs across launches + + private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private static final int URI_DOCS_ID = 1; + private static final int URI_DOCS_ID_CONTENTS = 2; + private static final int URI_SEARCH = 3; + + static { + sMatcher.addURI(AUTHORITY, "docs/#", URI_DOCS_ID); + sMatcher.addURI(AUTHORITY, "docs/#/contents", URI_DOCS_ID_CONTENTS); + sMatcher.addURI(AUTHORITY, "search", URI_SEARCH); + } + + @GuardedBy("mFiles") + private ArrayList mFiles = Lists.newArrayList(); + + @Override + public boolean onCreate() { + mFiles.clear(); + mFiles.add(Environment.getExternalStorageDirectory()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + + // TODO: support custom projections + projection = new String[] { + BaseColumns._ID, + DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE, DocumentColumns.GUID, + DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS }; + + final MatrixCursor cursor = new MatrixCursor(projection); + switch (sMatcher.match(uri)) { + case URI_DOCS_ID: { + final int id = Integer.parseInt(uri.getPathSegments().get(1)); + synchronized (mFiles) { + includeFileLocked(cursor, id); + } + break; + } + case URI_DOCS_ID_CONTENTS: { + final int parentId = Integer.parseInt(uri.getPathSegments().get(1)); + synchronized (mFiles) { + final File parent = mFiles.get(parentId); + for (File file : parent.listFiles()) { + final int id = findOrCreateFileLocked(file); + includeFileLocked(cursor, id); + } + } + break; + } + default: { + cursor.close(); + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } + + return cursor; + } + + private int findOrCreateFileLocked(File file) { + int id = mFiles.indexOf(file); + if (id == -1) { + id = mFiles.size(); + mFiles.add(file); + } + return id; + } + + private void includeFileLocked(MatrixCursor cursor, int id) { + final File file = mFiles.get(id); + int flags = 0; + + if (file.isDirectory() && file.canWrite()) { + flags |= DocumentsContract.FLAG_SUPPORTS_CREATE; + } + if (file.canWrite()) { + flags |= DocumentsContract.FLAG_SUPPORTS_RENAME; + } + + final String mimeType = getTypeLocked(id); + if (mimeType.startsWith("image/")) { + flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL; + } + + cursor.addRow(new Object[] { + id, file.getName(), file.length(), id, mimeType, file.lastModified(), flags }); + } + + @Override + public String getType(Uri uri) { + switch (sMatcher.match(uri)) { + case URI_DOCS_ID: { + final int id = Integer.parseInt(uri.getPathSegments().get(1)); + synchronized (mFiles) { + return getTypeLocked(id); + } + } + default: { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } + } + + private String getTypeLocked(int id) { + final File file = mFiles.get(id); + + if (file.isDirectory()) { + return DocumentsContract.MIME_TYPE_DIRECTORY; + } + + final int lastDot = file.getName().lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = file.getName().substring(lastDot + 1); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + switch (sMatcher.match(uri)) { + case URI_DOCS_ID: { + final int id = Integer.parseInt(uri.getPathSegments().get(1)); + synchronized (mFiles) { + final File file = mFiles.get(id); + // TODO: turn into thumbnail + return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode)); + } + } + default: { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } +} diff --git a/services/java/com/android/server/am/UriPermission.java b/services/java/com/android/server/am/UriPermission.java index e79faf944c7d3..cba8e0dbd37a0 100644 --- a/services/java/com/android/server/am/UriPermission.java +++ b/services/java/com/android/server/am/UriPermission.java @@ -217,13 +217,13 @@ class UriPermission { void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("userHandle=" + userHandle); - pw.print("sourcePkg=" + sourcePkg); - pw.println("targetPkg=" + targetPkg); + pw.print(" sourcePkg=" + sourcePkg); + pw.println(" targetPkg=" + targetPkg); pw.print(prefix); pw.print("modeFlags=0x" + Integer.toHexString(modeFlags)); - pw.print("globalModeFlags=0x" + Integer.toHexString(globalModeFlags)); - pw.println("persistedModeFlags=0x" + Integer.toHexString(persistedModeFlags)); + pw.print(" globalModeFlags=0x" + Integer.toHexString(globalModeFlags)); + pw.println(" persistedModeFlags=0x" + Integer.toHexString(persistedModeFlags)); if (mReadOwners != null) { pw.print(prefix);