public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
/** {@hide} */
- public static final String ACTION_ROOTS_CHANGED = "android.provider.action.ROOTS_CHANGED";
+ public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED";
/**
* {@link DocumentColumns#DOC_ID} value representing the root directory of a
* This signal is used to invalidate internal caches.
*/
public static void notifyRootsChanged(Context context, String authority) {
- final Intent intent = new Intent(ACTION_ROOTS_CHANGED);
+ final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED);
intent.setData(buildRootsUri(authority));
context.sendBroadcast(intent);
}
<intent-filter android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter android:priority="100">
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="*/*" />
+ </intent-filter>
+ <intent-filter android:priority="100">
+ <action android:name="android.intent.action.GET_CONTENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<string name="root_type_service">Services</string>
<string name="root_type_shortcut">Shortcuts</string>
<string name="root_type_device">Devices</string>
+ <string name="root_type_apps">More apps</string>
<string name="pref_advanced_devices">Display advanced devices</string>
<string name="pref_file_size">Display file size</string>
final DisplayState state = getDisplayState(DirectoryFragment.this);
mFilter = new MimePredicate(state.acceptMimes);
- final Uri contentsUri;
+ Uri contentsUri;
if (mType == TYPE_NORMAL) {
contentsUri = DocumentsContract.buildContentsUri(uri);
} else if (mType == TYPE_RECENT_OPEN) {
contentsUri = uri;
}
+ if (state.localOnly) {
+ contentsUri = DocumentsContract.setLocalOnly(contentsUri);
+ }
+
final Comparator<Document> sortOrder;
if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
sortOrder = new Document.DateComparator();
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
+import android.provider.DocumentsContract.DocumentColumns;
import android.util.Log;
import com.android.documentsui.model.Document;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.LinkedList;
import java.util.List;
public class DirectoryLoader extends UriDerivativeLoader<List<Document>> {
private Predicate<Document> mFilter;
private Comparator<Document> mSortOrder;
+ /**
+ * Stub result that represents an internal error.
+ */
+ public static class ExceptionResult extends LinkedList<Document> {
+ public final Exception e;
+
+ public ExceptionResult(Exception e) {
+ this.e = e;
+ }
+ }
+
public DirectoryLoader(Context context, Uri uri, int type, Predicate<Document> filter,
Comparator<Document> sortOrder) {
super(context, uri);
@Override
public List<Document> loadInBackground(Uri uri, CancellationSignal signal) {
+ try {
+ return loadInBackgroundInternal(uri, signal);
+ } catch (Exception e) {
+ return new ExceptionResult(e);
+ }
+ }
+
+ private List<Document> loadInBackgroundInternal(Uri uri, CancellationSignal signal) {
final ArrayList<Document> result = Lists.newArrayList();
- // TODO: send selection and sorting hints to backend
final ContentResolver resolver = getContext().getContentResolver();
- final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
+ final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
try {
while (cursor != null && cursor.moveToNext()) {
Document doc = null;
return result;
}
+
+ private String getQuerySortOrder() {
+ if (mSortOrder instanceof Document.DateComparator) {
+ return DocumentColumns.LAST_MODIFIED + " DESC";
+ } else if (mSortOrder instanceof Document.NameComparator) {
+ return DocumentColumns.DISPLAY_NAME + " ASC";
+ } else if (mSortOrder instanceof Document.SizeComparator) {
+ return DocumentColumns.SIZE + " DESC";
+ } else {
+ return null;
+ }
+ }
}
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ClipData;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
+import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
public static final int ACTION_OPEN = 1;
public static final int ACTION_CREATE = 2;
+ public static final int ACTION_GET_CONTENT = 3;
private int mAction;
final String action = intent.getAction();
if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
mAction = ACTION_OPEN;
- mDisplayState.allowMultiple = intent.getBooleanExtra(
- Intent.EXTRA_ALLOW_MULTIPLE, false);
} else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
mAction = ACTION_CREATE;
- mDisplayState.allowMultiple = false;
+ } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
+ mAction = ACTION_GET_CONTENT;
+ }
+
+ if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+ mDisplayState.allowMultiple = intent.getBooleanExtra(
+ Intent.EXTRA_ALLOW_MULTIPLE, false);
}
if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
mDisplayState.acceptMimes = new String[] { intent.getType() };
}
- if (MimePredicate.mimeMatches("image/*", mDisplayState.acceptMimes)) {
- mDisplayState.mode = DisplayState.MODE_GRID;
- } else {
- mDisplayState.mode = DisplayState.MODE_LIST;
- }
+ mDisplayState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
setResult(Activity.RESULT_CANCELED);
setContentView(R.layout.activity);
SaveFragment.show(getFragmentManager(), mimeType, title);
}
- RootsFragment.show(getFragmentManager());
+ if (mAction == ACTION_GET_CONTENT) {
+ final Intent moreApps = new Intent(getIntent());
+ moreApps.setComponent(null);
+ moreApps.setPackage(null);
+ RootsFragment.show(getFragmentManager(), moreApps);
+ } else {
+ RootsFragment.show(getFragmentManager(), null);
+ }
mRootsContainer = findViewById(R.id.container_roots);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setIcon(new ColorDrawable());
- if (mAction == ACTION_OPEN) {
+ if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
actionBar.setTitle(R.string.title_open);
} else if (mAction == ACTION_CREATE) {
actionBar.setTitle(R.string.title_save);
}
}
+ public void onAppPicked(ResolveInfo info) {
+ final Intent intent = new Intent(getIntent());
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ intent.setComponent(new ComponentName(
+ info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
+ startActivity(intent);
+ finish();
+ }
+
public void onDocumentPicked(Document doc) {
final FragmentManager fm = getFragmentManager();
if (doc.isDirectory()) {
mStack.push(doc);
onCurrentDirectoryChanged();
- } else if (mAction == ACTION_OPEN) {
+ } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
// Explicit file picked, return
onFinished(doc.uri);
} else if (mAction == ACTION_CREATE) {
values.put(RecentsProvider.COL_PATH, rawStack);
resolver.insert(RecentsProvider.buildRecentCreate(), values);
- } else if (mAction == ACTION_OPEN) {
+ } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
// Remember opened items
for (Uri uri : uris) {
values.clear();
intent.setClipData(clipData);
}
- // TODO: omit WRITE and PERSIST for GET_CONTENT
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
+ if (mAction == ACTION_GET_CONTENT) {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ } else {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
+ }
setResult(Activity.RESULT_OK, intent);
finish();
public int sortOrder = SORT_ORDER_NAME;
public boolean allowMultiple = false;
public boolean showSize = false;
+ public boolean localOnly = false;
public static final int MODE_LIST = 0;
public static final int MODE_GRID = 1;
switch (sMatcher.match(uri)) {
case URI_RECENT_OPEN: {
return db.query(TABLE_RECENT_OPEN, projection,
- buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
+ buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
}
case URI_RECENT_CREATE: {
return db.query(TABLE_RECENT_CREATE, projection,
- buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
+ buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
}
case URI_RESUME: {
final String packageName = uri.getPathSegments().get(1);
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);
+ // 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();
}
- } finally {
- cursor.close();
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to load some roots from " + info.providerInfo.authority
+ + ": " + e);
}
}
}
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.format.Formatter;
import com.android.documentsui.model.Root.RootComparator;
import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
/**
* Display list of known storage backend roots.
private ListView mList;
private SectionedRootsAdapter mAdapter;
- public static void show(FragmentManager fm) {
+ private static final String EXTRA_INCLUDE_APPS = "includeApps";
+
+ public static void show(FragmentManager fm, Intent includeApps) {
+ final Bundle args = new Bundle();
+ args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
+
final RootsFragment fragment = new RootsFragment();
+ fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container_roots, fragment);
final View view = inflater.inflate(R.layout.fragment_roots, container, false);
mList = (ListView) view.findViewById(android.R.id.list);
-
- mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots(context));
- mList.setAdapter(mAdapter);
mList.setOnItemClickListener(mItemListener);
+ final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
+ mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots(context), includeApps);
+
return view;
}
super.onStart();
final Context context = getActivity();
- mAdapter.setShowAdvanced(SettingsActivity.getDisplayAdvancedDevices(context));
+ mAdapter.updateVisible(SettingsActivity.getDisplayAdvancedDevices(context));
+ mList.setAdapter(mAdapter);
}
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);
+ final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
+ final Object item = mAdapter.getItem(position);
+ if (item instanceof Root) {
+ activity.onRootPicked((Root) item, true);
+ } else if (item instanceof ResolveInfo) {
+ activity.onAppPicked((ResolveInfo) item);
+ } else {
+ throw new IllegalStateException("Unknown root: " + item);
+ }
}
};
- public static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+ private static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
private int mHeaderId;
public RootsAdapter(Context context, int headerId) {
}
}
- public static class SectionedRootsAdapter extends SectionedListAdapter {
+ private static class AppsAdapter extends ArrayAdapter<ResolveInfo> implements SectionAdapter {
+ public AppsAdapter(Context context) {
+ super(context, 0);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Context context = parent.getContext();
+ final PackageManager pm = context.getPackageManager();
+ if (convertView == null) {
+ convertView = LayoutInflater.from(context)
+ .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 ResolveInfo info = getItem(position);
+ icon.setImageDrawable(info.loadIcon(pm));
+ title.setText(info.loadLabel(pm));
+
+ // TODO: match existing summary behavior from disambig dialog
+ summary.setVisibility(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(R.string.root_type_apps);
+
+ return convertView;
+ }
+ }
+
+ private static class SectionedRootsAdapter extends SectionedListAdapter {
private final RootsAdapter mServices;
private final RootsAdapter mShortcuts;
private final RootsAdapter mDevices;
private final RootsAdapter mDevicesAdvanced;
+ private final AppsAdapter mApps;
- public SectionedRootsAdapter(Context context, Collection<Root> roots) {
+ public SectionedRootsAdapter(Context context, Collection<Root> roots, Intent includeApps) {
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);
+ mApps = new AppsAdapter(context);
for (Root root : roots) {
Log.d(TAG, "Found rootType=" + root.rootType);
}
}
+ if (includeApps != null) {
+ final PackageManager pm = context.getPackageManager();
+ final List<ResolveInfo> infos = pm.queryIntentActivities(
+ includeApps, PackageManager.MATCH_DEFAULT_ONLY);
+
+ // Omit ourselves from the list
+ for (ResolveInfo info : infos) {
+ if (!context.getPackageName().equals(info.activityInfo.packageName)) {
+ mApps.add(info);
+ }
+ }
+ }
+
final RootComparator comp = new RootComparator();
mServices.sort(comp);
mShortcuts.sort(comp);
mDevicesAdvanced.sort(comp);
}
- public void setShowAdvanced(boolean showAdvanced) {
+ public void updateVisible(boolean showAdvanced) {
clearSections();
if (mServices.getCount() > 0) {
addSection(mServices);
if (devices.getCount() > 0) {
addSection(devices);
}
+
+ if (mApps.getCount() > 0) {
+ addSection(mApps);
+ }
}
}
}
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
notifyDataSetChanged();
}
+ /**
+ * After mutating sections, you <em>must</em>
+ * {@link AdapterView#setAdapter(android.widget.Adapter)} to correctly
+ * recount view types.
+ */
public void addSection(SectionAdapter adapter) {
mSections.add(adapter);
notifyDataSetChanged();
if (position == 0) {
return false;
} else if (position < sectionSize) {
- return section.isEnabled(position);
+ return section.isEnabled(position - 1);
}
// Otherwise jump into next section
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
if (multiple.isChecked()) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
if (multiple.isChecked()) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
"text/plain", "application/msword" });
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt");
startActivityForResult(intent, 42);
});
view.addView(button);
+ button = new Button(context);
+ button.setText("GET_CONTENT */*");
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ if (multiple.isChecked()) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ }
+ startActivityForResult(Intent.createChooser(intent, "Kittens!"), 42);
+ }
+ });
+ view.addView(button);
+
mResult = new TextView(context);
view.addView(mResult);
is = getContentResolver().openInputStream(uri);
final int length = Streams.readFullyNoClose(is).length;
Log.d(TAG, "read length=" + length);
- } catch (IOException e) {
+ } catch (Exception e) {
Log.w(TAG, "Failed to read " + uri, e);
} finally {
IoUtils.closeQuietly(is);