Freshen restored stacks, more UX bug fixes.

When restoring a persisted stack, such as last location or a location
where save occurred, freshen the DocumentStack to get the latest
details from backend, and ensure the path is still valid.

Filter Recent directories in create mode based on roots allowed by
incoming request.

Remember when user last picked an external app, and open drawer next
time user launches for GET_CONTENT.

Fix state list drawable ordering, and avoid clobbering Drawable
callback when wrapping in InsetDrawable; tricksy hobbitses!

Make grid items smaller to always fit two columns on phones.  Draw
grid items all the way to screen edge; don't clip to padding.  Better
error message when folder creation failed.  Show Recents in grid mode
when picking any visual content, not just images.

Bug: 10846100, 10903211, 10898993, 10892808, 10892058, 10892009, 10885012
Change-Id: Ia0f88d911abc6ea03645d6fd3e04271c21d5936a
This commit is contained in:
Jeff Sharkey
2013-09-24 12:07:12 -07:00
parent b180a65d41
commit deffadeb74
12 changed files with 199 additions and 61 deletions

View File

@@ -16,18 +16,16 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:state_selected="true" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
<item android:state_enabled="false" android:state_focused="true" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
<item android:state_enabled="false" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
<item android:state_window_focused="false" android:drawable="@android:color/transparent" />
<item android:state_activated="true" android:state_pressed="true" android:drawable="@*android:drawable/list_activated_holo" />
<item android:state_activated="true" android:drawable="@*android:drawable/list_activated_holo" />
<item android:state_focused="true" android:state_activated="true" android:drawable="@*android:drawable/list_selected_holo_light" />
<item android:state_focused="false" android:state_activated="true" android:drawable="@*android:drawable/list_selected_holo_light" />
<item android:state_focused="true" android:drawable="@*android:drawable/list_focused_holo" />
<item android:state_selected="true" android:drawable="@*android:drawable/list_focused_holo" />
<item android:state_pressed="true" android:state_focused="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
<item android:state_pressed="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
<item android:state_focused="true" android:state_enabled="false" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
<item android:state_focused="true" android:state_enabled="false" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
<item android:state_focused="true" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
<item android:state_focused="false" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
<item android:state_focused="true" android:drawable="@*android:drawable/list_focused_holo" />
<item android:drawable="@android:color/transparent" />

View File

@@ -42,6 +42,7 @@
android:paddingEnd="@dimen/grid_padding_horiz"
android:paddingTop="@dimen/grid_padding"
android:paddingBottom="@dimen/grid_padding"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
android:listSelector="@android:color/transparent"
android:visibility="gone" />

View File

@@ -17,8 +17,8 @@
<resources>
<dimen name="icon_size">32dp</dimen>
<dimen name="root_icon_size">24dp</dimen>
<dimen name="grid_width">180dp</dimen>
<dimen name="grid_height">180dp</dimen>
<dimen name="grid_width">160dp</dimen>
<dimen name="grid_height">170dp</dimen>
<dimen name="grid_padding">4dp</dimen>
<dimen name="grid_padding_horiz">4dp</dimen>

View File

@@ -62,6 +62,8 @@
<!-- Toast shown when saving a document failed with an error [CHAR LIMIT=48] -->
<string name="save_error">Failed to save document</string>
<!-- Toast shown when creating a folder failed with an error [CHAR LIMIT=48] -->
<string name="create_error">Failed to create folder</string>
<!-- Title of storage root location that contains recently modified or used documents [CHAR LIMIT=24] -->
<string name="root_recent">Recent</string>

View File

@@ -76,7 +76,7 @@ public class CreateDirectoryFragment extends DialogFragment {
final DocumentInfo childDoc = DocumentInfo.fromUri(resolver, childUri);
activity.onDocumentPicked(childDoc);
} catch (Exception e) {
Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.create_error, Toast.LENGTH_SHORT).show();
}
}
});

View File

@@ -715,8 +715,16 @@ public class DirectoryFragment extends Fragment {
final FrameLayout grid = (FrameLayout) convertView;
final int gridPadding = getResources()
.getDimensionPixelSize(R.dimen.grid_padding);
grid.setForeground(new InsetDrawable(grid.getForeground(), gridPadding));
grid.setBackground(new InsetDrawable(grid.getBackground(), gridPadding));
// Tricksy hobbitses! We need to fully clear the drawable so
// the view doesn't clobber the new InsetDrawable callback
// when setting back later.
final Drawable fg = grid.getForeground();
final Drawable bg = grid.getBackground();
grid.setForeground(null);
grid.setBackground(null);
grid.setForeground(new InsetDrawable(fg, gridPadding));
grid.setBackground(new InsetDrawable(bg, gridPadding));
} else {
throw new IllegalStateException();
}

View File

@@ -285,6 +285,7 @@ public class DocumentsActivity extends Activity {
private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
private volatile boolean mRestoredStack;
private volatile boolean mExternal;
@Override
protected Void doInBackground(Void... params) {
@@ -298,6 +299,7 @@ public class DocumentsActivity extends Activity {
cursor.getColumnIndex(ResumeColumns.STACK));
DurableUtils.readFromArray(rawStack, mState.stack);
mRestoredStack = true;
mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
}
} catch (IOException e) {
Log.w(TAG, "Failed to resume", e);
@@ -305,12 +307,17 @@ public class DocumentsActivity extends Activity {
IoUtils.closeQuietly(cursor);
}
// If restored root isn't valid, fall back to recents
final RootInfo root = getCurrentRoot();
final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
if (!matchingRoots.contains(root)) {
mState.stack.reset();
mRestoredStack = false;
if (mRestoredStack) {
// Update the restored stack to ensure we have freshest data
final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
try {
mState.stack.updateRoot(matchingRoots);
mState.stack.updateDocuments(getContentResolver());
} catch (FileNotFoundException e) {
Log.w(TAG, "Failed to restore stack: " + e);
mState.stack.reset();
mRestoredStack = false;
}
}
return null;
@@ -321,10 +328,22 @@ public class DocumentsActivity extends Activity {
if (isDestroyed()) return;
mState.restored = true;
// Only open drawer when not restoring stack, and when not showing
// visual content.
if (!mRestoredStack
&& !MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
// Show drawer when no stack restored, but only when requesting
// non-visual content. However, if we last used an external app,
// drawer is always shown.
boolean showDrawer = false;
if (!mRestoredStack) {
showDrawer = true;
}
if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
showDrawer = false;
}
if (mExternal && mState.action == ACTION_GET_CONTENT) {
showDrawer = true;
}
if (showDrawer) {
setRootsDrawerOpen(true);
}
@@ -340,6 +359,7 @@ public class DocumentsActivity extends Activity {
mState.showSize = true;
} else {
mState.showSize = SettingsActivity.getDisplayFileSize(this);
invalidateOptionsMenu();
}
}
@@ -779,10 +799,10 @@ public class DocumentsActivity extends Activity {
} else {
DirectoryFragment.showRecentsOpen(fm, anim);
// Start recents in relevant mode
final boolean acceptImages = MimePredicate.mimeMatches(
"image/*", mState.acceptMimes);
mState.userMode = acceptImages ? MODE_GRID : MODE_LIST;
// Start recents in grid when requesting visual things
final boolean visualMimes = MimePredicate.mimeMatches(
MimePredicate.VISUAL_MIMES, mState.acceptMimes);
mState.userMode = visualMimes ? MODE_GRID : MODE_LIST;
mState.derivedMode = mState.userMode;
}
} else {
@@ -814,9 +834,17 @@ public class DocumentsActivity extends Activity {
}
public void onStackPicked(DocumentStack stack) {
mState.stack = stack;
mState.stackTouched = true;
onCurrentDirectoryChanged(ANIM_SIDE);
try {
// Update the restored stack to ensure we have freshest data
stack.updateDocuments(getContentResolver());
mState.stack = stack;
mState.stackTouched = true;
onCurrentDirectoryChanged(ANIM_SIDE);
} catch (FileNotFoundException e) {
Log.w(TAG, "Failed to restore stack: " + e);
}
}
public void onRootPicked(RootInfo root, boolean closeDrawer) {
@@ -858,6 +886,14 @@ public class DocumentsActivity extends Activity {
// Only relay back results when not canceled; otherwise stick around to
// let the user pick another app/backend.
if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
// Remember that we last picked via external app
final String packageName = getCallingPackage();
final ContentValues values = new ContentValues();
values.put(ResumeColumns.EXTERNAL, 1);
getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
// Pass back result to original caller
setResult(resultCode, data);
finish();
} else {
@@ -945,6 +981,7 @@ public class DocumentsActivity extends Activity {
final String packageName = getCallingPackage();
values.clear();
values.put(ResumeColumns.STACK, rawStack);
values.put(ResumeColumns.EXTERNAL, 0);
resolver.insert(RecentsProvider.buildResume(packageName), values);
final Intent intent = new Intent();

View File

@@ -45,8 +45,10 @@ import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.documentsui.DocumentsActivity.State;
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
@@ -55,6 +57,7 @@ import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -89,10 +92,13 @@ public class RecentsCreateFragment extends Fragment {
mAdapter = new DocumentStackAdapter();
mListView.setAdapter(mAdapter);
final RootsCache roots = DocumentsApplication.getRootsCache(context);
final State state = ((DocumentsActivity) getActivity()).getDisplayState();
mCallbacks = new LoaderCallbacks<List<DocumentStack>>() {
@Override
public Loader<List<DocumentStack>> onCreateLoader(int id, Bundle args) {
return new RecentsCreateLoader(context);
return new RecentsCreateLoader(context, roots, state);
}
@Override
@@ -131,12 +137,18 @@ public class RecentsCreateFragment extends Fragment {
};
public static class RecentsCreateLoader extends UriDerivativeLoader<Uri, List<DocumentStack>> {
public RecentsCreateLoader(Context context) {
private final RootsCache mRoots;
private final State mState;
public RecentsCreateLoader(Context context, RootsCache roots, State state) {
super(context, RecentsProvider.buildRecent());
mRoots = roots;
mState = state;
}
@Override
public List<DocumentStack> loadInBackground(Uri uri, CancellationSignal signal) {
final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
final ArrayList<DocumentStack> result = Lists.newArrayList();
final ContentResolver resolver = getContext().getContentResolver();
@@ -149,6 +161,12 @@ public class RecentsCreateFragment extends Fragment {
try {
final DocumentStack stack = new DocumentStack();
stack.read(new DataInputStream(new ByteArrayInputStream(rawStack)));
// Only update root here to avoid spinning up all
// providers; we update the stack during the actual
// restore. This also filters away roots that don't
// match current filter.
stack.updateRoot(matchingRoots);
result.add(stack);
} catch (IOException e) {
Log.w(TAG, "Failed to resolve stack: " + e);

View File

@@ -72,6 +72,7 @@ public class RecentsProvider extends ContentProvider {
public static final String PACKAGE_NAME = "package_name";
public static final String STACK = "stack";
public static final String TIMESTAMP = "timestamp";
public static final String EXTERNAL = "external";
}
public static Uri buildRecent() {
@@ -97,9 +98,10 @@ public class RecentsProvider extends ContentProvider {
private static final int VERSION_INIT = 1;
private static final int VERSION_AS_BLOB = 3;
private static final int VERSION_ADD_EXTERNAL = 4;
public DatabaseHelper(Context context) {
super(context, DB_NAME, null, VERSION_AS_BLOB);
super(context, DB_NAME, null, VERSION_ADD_EXTERNAL);
}
@Override
@@ -121,9 +123,10 @@ public class RecentsProvider extends ContentProvider {
")");
db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
ResumeColumns.PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
ResumeColumns.STACK + " BLOB," +
ResumeColumns.TIMESTAMP + " INTEGER" +
ResumeColumns.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY," +
ResumeColumns.STACK + " BLOB DEFAULT NULL," +
ResumeColumns.TIMESTAMP + " INTEGER," +
ResumeColumns.EXTERNAL + " INTEGER NOT NULL DEFAULT 0" +
")");
}
@@ -176,6 +179,7 @@ public class RecentsProvider extends ContentProvider {
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mHelper.getWritableDatabase();
final ContentValues key = new ContentValues();
switch (sMatcher.match(uri)) {
case URI_RECENT:
values.put(RecentColumns.TIMESTAMP, System.currentTimeMillis());
@@ -188,7 +192,6 @@ public class RecentsProvider extends ContentProvider {
final String rootId = uri.getPathSegments().get(2);
final String documentId = uri.getPathSegments().get(3);
final ContentValues key = new ContentValues();
key.put(StateColumns.AUTHORITY, authority);
key.put(StateColumns.ROOT_ID, rootId);
key.put(StateColumns.DOCUMENT_ID, documentId);
@@ -201,10 +204,15 @@ public class RecentsProvider extends ContentProvider {
return uri;
case URI_RESUME:
final String packageName = uri.getPathSegments().get(1);
values.put(ResumeColumns.PACKAGE_NAME, packageName);
values.put(ResumeColumns.TIMESTAMP, System.currentTimeMillis());
db.insert(TABLE_RESUME, null, values);
final String packageName = uri.getPathSegments().get(1);
key.put(ResumeColumns.PACKAGE_NAME, packageName);
// Ensure that row exists, then update with changed values
db.insertWithOnConflict(TABLE_RESUME, null, key, SQLiteDatabase.CONFLICT_IGNORE);
db.update(TABLE_RESUME, values, ResumeColumns.PACKAGE_NAME + "=?",
new String[] { packageName });
return uri;
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);

View File

@@ -27,6 +27,7 @@ import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import libcore.io.IoUtils;
@@ -52,6 +53,9 @@ public class TestActivity extends Activity {
final LinearLayout view = new LinearLayout(context);
view.setOrientation(LinearLayout.VERTICAL);
mResult = new TextView(context);
view.addView(mResult);
final CheckBox multiple = new CheckBox(context);
multiple.setText("ALLOW_MULTIPLE");
view.addView(multiple);
@@ -155,6 +159,23 @@ public class TestActivity extends Activity {
});
view.addView(button);
button = new Button(context);
button.setText("CREATE_DOC image/png");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/png");
intent.putExtra(Intent.EXTRA_TITLE, "mypicture.png");
if (localOnly.isChecked()) {
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
}
startActivityForResult(intent, CODE_WRITE);
}
});
view.addView(button);
button = new Button(context);
button.setText("GET_CONTENT */*");
button.setOnClickListener(new OnClickListener() {
@@ -174,10 +195,10 @@ public class TestActivity extends Activity {
});
view.addView(button);
mResult = new TextView(context);
view.addView(mResult);
final ScrollView scroll = new ScrollView(context);
scroll.addView(view);
setContentView(view);
setContentView(scroll);
}
@Override

View File

@@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsProvider;
import android.provider.DocumentsContract.Document;
import com.android.documentsui.RootCursorWrapper;
@@ -141,23 +142,42 @@ public class DocumentInfo implements Durable, Parcelable {
}
public static DocumentInfo fromCursor(Cursor cursor, String authority) {
final DocumentInfo doc = new DocumentInfo();
doc.authority = authority;
doc.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
doc.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
doc.deriveFields();
return doc;
final DocumentInfo info = new DocumentInfo();
info.updateFromCursor(cursor, authority);
return info;
}
public static DocumentInfo fromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException {
public void updateFromCursor(Cursor cursor, String authority) {
this.authority = authority;
this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
this.size = getCursorLong(cursor, Document.COLUMN_SIZE);
this.icon = getCursorInt(cursor, Document.COLUMN_ICON);
this.deriveFields();
}
public static DocumentInfo fromUri(ContentResolver resolver, Uri uri)
throws FileNotFoundException {
final DocumentInfo info = new DocumentInfo();
info.updateFromUri(resolver, uri);
return info;
}
/**
* Update a possibly stale restored document against a live
* {@link DocumentsProvider}.
*/
public void updateSelf(ContentResolver resolver) throws FileNotFoundException {
updateFromUri(resolver, derivedUri);
}
public void updateFromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException {
final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
uri.getAuthority());
Cursor cursor = null;
@@ -166,7 +186,7 @@ public class DocumentInfo implements Durable, Parcelable {
if (!cursor.moveToFirst()) {
throw new FileNotFoundException("Missing details for " + uri);
}
return fromCursor(cursor, uri.getAuthority());
updateFromCursor(cursor, uri.getAuthority());
} catch (Throwable t) {
throw asFileNotFoundException(t);
} finally {

View File

@@ -16,10 +16,15 @@
package com.android.documentsui.model;
import android.content.ContentResolver;
import android.provider.DocumentsProvider;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Collection;
import java.util.LinkedList;
/**
@@ -46,6 +51,26 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
return size() == 0;
}
public void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
for (RootInfo root : matchingRoots) {
if (root.equals(this.root)) {
this.root = root;
return;
}
}
throw new FileNotFoundException("Failed to find matching root for " + root);
}
/**
* Update a possibly stale restored stack against a live
* {@link DocumentsProvider}.
*/
public void updateDocuments(ContentResolver resolver) throws FileNotFoundException {
for (DocumentInfo info : this) {
info.updateSelf(resolver);
}
}
@Override
public void reset() {
clear();