Load RootInfo in background, invalidation.
Move all RootInfo queries to background threads to avoid janking the UI. Update passes happen on spawed task, which swaps out updated cache results when finished. Support partial updates when only a single package/authority has changed. Watch for change notifications for roots, since flags can change over time. Ignore stopped packages when in background, but query them for roots when launching any picker UI. Optimize management launches by treating as one-shot requests that don't need to wait for all RootInfo. Bug: 10600454, 10745490 Change-Id: Ibc7b15688ef6b41bd7e9dd0d7564b501e60e49a9
This commit is contained in:
@@ -48,14 +48,6 @@
|
|||||||
android:authorities="com.android.documentsui.recents"
|
android:authorities="com.android.documentsui.recents"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".DocumentChangedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.provider.action.DOCUMENT_CHANGED" />
|
|
||||||
<data android:mimeType="vnd.android.cursor.dir/root" />
|
|
||||||
<data android:mimeType="vnd.android.cursor.item/root" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- TODO: remove when we have real clients -->
|
<!-- TODO: remove when we have real clients -->
|
||||||
<activity android:name=".TestActivity" android:enabled="false">
|
<activity android:name=".TestActivity" android:enabled="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -249,8 +249,7 @@ public class DirectoryFragment extends Fragment {
|
|||||||
context, mType, root, doc, contentsUri, state.userSortOrder);
|
context, mType, root, doc, contentsUri, state.userSortOrder);
|
||||||
case TYPE_RECENT_OPEN:
|
case TYPE_RECENT_OPEN:
|
||||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
||||||
final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
|
return new RecentLoader(context, roots, state);
|
||||||
return new RecentLoader(context, matchingRoots, state.acceptMimes);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unknown type " + mType);
|
throw new IllegalStateException("Unknown type " + mType);
|
||||||
}
|
}
|
||||||
@@ -797,7 +796,9 @@ public class DirectoryFragment extends Fragment {
|
|||||||
|
|
||||||
Drawable iconDrawable = null;
|
Drawable iconDrawable = null;
|
||||||
if (mType == TYPE_RECENT_OPEN) {
|
if (mType == TYPE_RECENT_OPEN) {
|
||||||
final RootInfo root = roots.getRoot(docAuthority, docRootId);
|
// We've already had to enumerate roots before any results can
|
||||||
|
// be shown, so this will never block.
|
||||||
|
final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
|
||||||
iconDrawable = root.loadIcon(context);
|
iconDrawable = root.loadIcon(context);
|
||||||
|
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
@@ -808,7 +809,7 @@ public class DirectoryFragment extends Fragment {
|
|||||||
summary.setVisibility(View.VISIBLE);
|
summary.setVisibility(View.VISIBLE);
|
||||||
hasLine2 = true;
|
hasLine2 = true;
|
||||||
} else {
|
} else {
|
||||||
if (iconDrawable != null && roots.isIconUnique(root)) {
|
if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
|
||||||
// No summary needed if icon speaks for itself
|
// No summary needed if icon speaks for itself
|
||||||
summary.setVisibility(View.INVISIBLE);
|
summary.setVisibility(View.INVISIBLE);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles changes which invalidate cached data.
|
|
||||||
*/
|
|
||||||
public class DocumentChangedReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
Log.d(TAG, "Regenerating roots cache");
|
|
||||||
DocumentsApplication.getRootsCache(context).update();
|
|
||||||
// TODO: invalidate cached data in recents provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,6 +46,7 @@ import android.graphics.drawable.ColorDrawable;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.InsetDrawable;
|
import android.graphics.drawable.InsetDrawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
@@ -87,6 +88,7 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class DocumentsActivity extends Activity {
|
public class DocumentsActivity extends Activity {
|
||||||
@@ -207,7 +209,16 @@ public class DocumentsActivity extends Activity {
|
|||||||
RootsFragment.show(getFragmentManager(), null);
|
RootsFragment.show(getFragmentManager(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentDirectoryChanged(ANIM_NONE);
|
if (!mState.restored) {
|
||||||
|
if (mState.action == ACTION_MANAGE) {
|
||||||
|
final Uri rootUri = getIntent().getData();
|
||||||
|
new RestoreRootTask(rootUri).execute();
|
||||||
|
} else {
|
||||||
|
new RestoreStackTask().execute();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onCurrentDirectoryChanged(ANIM_NONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildDefaultState() {
|
private void buildDefaultState() {
|
||||||
@@ -241,22 +252,41 @@ public class DocumentsActivity extends Activity {
|
|||||||
|
|
||||||
mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
|
mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
|
||||||
mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this);
|
mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
|
||||||
|
private Uri mRootUri;
|
||||||
|
|
||||||
|
public RestoreRootTask(Uri rootUri) {
|
||||||
|
mRootUri = rootUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RootInfo doInBackground(Void... params) {
|
||||||
|
final String rootId = DocumentsContract.getRootId(mRootUri);
|
||||||
|
return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(RootInfo root) {
|
||||||
|
if (isDestroyed()) return;
|
||||||
|
mState.restored = true;
|
||||||
|
|
||||||
if (mState.action == ACTION_MANAGE) {
|
|
||||||
final Uri uri = intent.getData();
|
|
||||||
final String rootId = DocumentsContract.getRootId(uri);
|
|
||||||
final RootInfo root = mRoots.getRoot(uri.getAuthority(), rootId);
|
|
||||||
if (root != null) {
|
if (root != null) {
|
||||||
onRootPicked(root, true);
|
onRootPicked(root, true);
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to find root: " + uri);
|
Log.w(TAG, "Failed to find root: " + mRootUri);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
private volatile boolean mRestoredStack;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params) {
|
||||||
// Restore last stack for calling package
|
// Restore last stack for calling package
|
||||||
// TODO: move into async loader
|
|
||||||
boolean restoredStack = false;
|
|
||||||
final String packageName = getCallingPackage();
|
final String packageName = getCallingPackage();
|
||||||
final Cursor cursor = getContentResolver()
|
final Cursor cursor = getContentResolver()
|
||||||
.query(RecentsProvider.buildResume(packageName), null, null, null, null);
|
.query(RecentsProvider.buildResume(packageName), null, null, null, null);
|
||||||
@@ -265,7 +295,7 @@ public class DocumentsActivity extends Activity {
|
|||||||
final byte[] rawStack = cursor.getBlob(
|
final byte[] rawStack = cursor.getBlob(
|
||||||
cursor.getColumnIndex(ResumeColumns.STACK));
|
cursor.getColumnIndex(ResumeColumns.STACK));
|
||||||
DurableUtils.readFromArray(rawStack, mState.stack);
|
DurableUtils.readFromArray(rawStack, mState.stack);
|
||||||
restoredStack = true;
|
mRestoredStack = true;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, "Failed to resume", e);
|
Log.w(TAG, "Failed to resume", e);
|
||||||
@@ -275,18 +305,28 @@ public class DocumentsActivity extends Activity {
|
|||||||
|
|
||||||
// If restored root isn't valid, fall back to recents
|
// If restored root isn't valid, fall back to recents
|
||||||
final RootInfo root = getCurrentRoot();
|
final RootInfo root = getCurrentRoot();
|
||||||
final List<RootInfo> matchingRoots = mRoots.getMatchingRoots(mState);
|
final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
|
||||||
if (!matchingRoots.contains(root)) {
|
if (!matchingRoots.contains(root)) {
|
||||||
mState.stack.reset();
|
mState.stack.reset();
|
||||||
restoredStack = false;
|
mRestoredStack = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void result) {
|
||||||
|
if (isDestroyed()) return;
|
||||||
|
mState.restored = true;
|
||||||
|
|
||||||
// Only open drawer when not restoring stack, and when not showing
|
// Only open drawer when not restoring stack, and when not showing
|
||||||
// visual content.
|
// visual content.
|
||||||
if (!restoredStack
|
if (!mRestoredStack
|
||||||
&& !MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
|
&& !MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
|
||||||
setRootsDrawerOpen(true);
|
setRootsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCurrentDirectoryChanged(ANIM_NONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +975,7 @@ public class DocumentsActivity extends Activity {
|
|||||||
public boolean localOnly = false;
|
public boolean localOnly = false;
|
||||||
public boolean showAdvanced = false;
|
public boolean showAdvanced = false;
|
||||||
public boolean stackTouched = false;
|
public boolean stackTouched = false;
|
||||||
|
public boolean restored = false;
|
||||||
|
|
||||||
/** Current user navigation stack; empty implies recents. */
|
/** Current user navigation stack; empty implies recents. */
|
||||||
public DocumentStack stack = new DocumentStack();
|
public DocumentStack stack = new DocumentStack();
|
||||||
@@ -974,6 +1015,7 @@ public class DocumentsActivity extends Activity {
|
|||||||
out.writeInt(localOnly ? 1 : 0);
|
out.writeInt(localOnly ? 1 : 0);
|
||||||
out.writeInt(showAdvanced ? 1 : 0);
|
out.writeInt(showAdvanced ? 1 : 0);
|
||||||
out.writeInt(stackTouched ? 1 : 0);
|
out.writeInt(stackTouched ? 1 : 0);
|
||||||
|
out.writeInt(restored ? 1 : 0);
|
||||||
DurableUtils.writeToParcel(out, stack);
|
DurableUtils.writeToParcel(out, stack);
|
||||||
out.writeString(currentSearch);
|
out.writeString(currentSearch);
|
||||||
out.writeMap(dirState);
|
out.writeMap(dirState);
|
||||||
@@ -992,6 +1034,7 @@ public class DocumentsActivity extends Activity {
|
|||||||
state.localOnly = in.readInt() != 0;
|
state.localOnly = in.readInt() != 0;
|
||||||
state.showAdvanced = in.readInt() != 0;
|
state.showAdvanced = in.readInt() != 0;
|
||||||
state.stackTouched = in.readInt() != 0;
|
state.stackTouched = in.readInt() != 0;
|
||||||
|
state.restored = in.readInt() != 0;
|
||||||
DurableUtils.readFromParcel(in, state.stack);
|
DurableUtils.readFromParcel(in, state.stack);
|
||||||
state.currentSearch = in.readString();
|
state.currentSearch = in.readString();
|
||||||
in.readMap(state.dirState, null);
|
in.readMap(state.dirState, null);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
public class DocumentsApplication extends Application {
|
public class DocumentsApplication extends Application {
|
||||||
private RootsCache mRoots;
|
private RootsCache mRoots;
|
||||||
@@ -49,6 +50,8 @@ public class DocumentsApplication extends Application {
|
|||||||
final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
|
final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
|
||||||
|
|
||||||
mRoots = new RootsCache(this);
|
mRoots = new RootsCache(this);
|
||||||
|
mRoots.updateAsync();
|
||||||
|
|
||||||
mThumbnails = new ThumbnailCache(memoryClassBytes / 4);
|
mThumbnails = new ThumbnailCache(memoryClassBytes / 4);
|
||||||
|
|
||||||
final IntentFilter packageFilter = new IntentFilter();
|
final IntentFilter packageFilter = new IntentFilter();
|
||||||
@@ -77,8 +80,13 @@ public class DocumentsApplication extends Application {
|
|||||||
private BroadcastReceiver mCacheReceiver = new BroadcastReceiver() {
|
private BroadcastReceiver mCacheReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
// TODO: narrow changed/removed to only packages that have backends
|
final Uri data = intent.getData();
|
||||||
mRoots.update();
|
if (data != null) {
|
||||||
|
final String packageName = data.getSchemeSpecificPart();
|
||||||
|
mRoots.updatePackageAsync(packageName);
|
||||||
|
} else {
|
||||||
|
mRoots.updateAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import libcore.io.IoUtils;
|
|||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@@ -80,8 +81,8 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final List<RootInfo> mRoots;
|
private final RootsCache mRoots;
|
||||||
private final String[] mAcceptMimes;
|
private final State mState;
|
||||||
|
|
||||||
private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
|
private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
|
||||||
|
|
||||||
@@ -138,10 +139,10 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecentLoader(Context context, List<RootInfo> roots, String[] acceptMimes) {
|
public RecentLoader(Context context, RootsCache roots, State state) {
|
||||||
super(context);
|
super(context);
|
||||||
mRoots = roots;
|
mRoots = roots;
|
||||||
mAcceptMimes = acceptMimes;
|
mState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -150,7 +151,8 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
|
|||||||
// First time through we kick off all the recent tasks, and wait
|
// First time through we kick off all the recent tasks, and wait
|
||||||
// around to see if everyone finishes quickly.
|
// around to see if everyone finishes quickly.
|
||||||
|
|
||||||
for (RootInfo root : mRoots) {
|
final Collection<RootInfo> roots = mRoots.getMatchingRootsBlocking(mState);
|
||||||
|
for (RootInfo root : roots) {
|
||||||
if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
|
if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
|
||||||
final RecentTask task = new RecentTask(root.authority, root.rootId);
|
final RecentTask task = new RecentTask(root.authority, root.rootId);
|
||||||
mTasks.put(root, task);
|
mTasks.put(root, task);
|
||||||
@@ -177,7 +179,7 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
|
|||||||
try {
|
try {
|
||||||
final Cursor cursor = task.get();
|
final Cursor cursor = task.get();
|
||||||
final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
|
final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
|
||||||
cursor, mAcceptMimes, new String[] { Document.MIME_TYPE_DIR }) {
|
cursor, mState.acceptMimes, new String[] { Document.MIME_TYPE_DIR }) {
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
// Ignored, since we manage cursor lifecycle internally
|
// Ignored, since we manage cursor lifecycle internally
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ public class RecentsCreateFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
final Context context = parent.getContext();
|
final Context context = parent.getContext();
|
||||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
|
||||||
|
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
|||||||
@@ -21,10 +21,15 @@ import static com.android.documentsui.DocumentsActivity.TAG;
|
|||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ProviderInfo;
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.DocumentsContract.Root;
|
import android.provider.DocumentsContract.Root;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -32,114 +37,267 @@ import android.util.Log;
|
|||||||
import com.android.documentsui.DocumentsActivity.State;
|
import com.android.documentsui.DocumentsActivity.State;
|
||||||
import com.android.documentsui.model.RootInfo;
|
import com.android.documentsui.model.RootInfo;
|
||||||
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.GuardedBy;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.internal.util.Objects;
|
import com.android.internal.util.Objects;
|
||||||
import com.google.android.collect.Lists;
|
import com.google.android.collect.Lists;
|
||||||
|
import com.google.android.collect.Sets;
|
||||||
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
import libcore.io.IoUtils;
|
import libcore.io.IoUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of known storage backends and their roots.
|
* Cache of known storage backends and their roots.
|
||||||
*/
|
*/
|
||||||
public class RootsCache {
|
public class RootsCache {
|
||||||
|
private static final boolean LOGD = true;
|
||||||
|
|
||||||
// TODO: cache roots in local provider to avoid spinning up backends
|
// TODO: cache roots in local provider to avoid spinning up backends
|
||||||
// TODO: root updates should trigger UI refresh
|
// TODO: root updates should trigger UI refresh
|
||||||
|
|
||||||
private static final boolean RECENTS_ENABLED = true;
|
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
private final ContentObserver mObserver;
|
||||||
|
|
||||||
public List<RootInfo> mRoots = Lists.newArrayList();
|
private final RootInfo mRecentsRoot = new RootInfo();
|
||||||
|
|
||||||
private RootInfo mRecentsRoot;
|
private final Object mLock = new Object();
|
||||||
|
private final CountDownLatch mFirstLoad = new CountDownLatch(1);
|
||||||
|
|
||||||
|
@GuardedBy("mLock")
|
||||||
|
private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
|
||||||
|
@GuardedBy("mLock")
|
||||||
|
private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
|
||||||
|
|
||||||
|
@GuardedBy("mObservedAuthorities")
|
||||||
|
private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
|
||||||
|
|
||||||
public RootsCache(Context context) {
|
public RootsCache(Context context) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
update();
|
mObserver = new RootsChangedObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RootsChangedObserver extends ContentObserver {
|
||||||
|
public RootsChangedObserver() {
|
||||||
|
super(new Handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChange(boolean selfChange, Uri uri) {
|
||||||
|
if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri);
|
||||||
|
updateAuthorityAsync(uri.getAuthority());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gather roots from all known storage providers.
|
* Gather roots from all known storage providers.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("ActivityThread")
|
public void updateAsync() {
|
||||||
public void update() {
|
// Special root for recents
|
||||||
mRoots.clear();
|
mRecentsRoot.rootType = Root.ROOT_TYPE_SHORTCUT;
|
||||||
|
mRecentsRoot.icon = R.drawable.ic_root_recent;
|
||||||
|
mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
|
||||||
|
mRecentsRoot.title = mContext.getString(R.string.root_recent);
|
||||||
|
mRecentsRoot.availableBytes = -1;
|
||||||
|
|
||||||
if (RECENTS_ENABLED) {
|
new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
// Create special root for recents
|
}
|
||||||
final RootInfo root = new RootInfo();
|
|
||||||
root.rootType = Root.ROOT_TYPE_SHORTCUT;
|
|
||||||
root.icon = R.drawable.ic_root_recent;
|
|
||||||
root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
|
|
||||||
root.title = mContext.getString(R.string.root_recent);
|
|
||||||
root.availableBytes = -1;
|
|
||||||
|
|
||||||
mRoots.add(root);
|
/**
|
||||||
mRecentsRoot = root;
|
* Gather roots from storage providers belonging to given package name.
|
||||||
|
*/
|
||||||
|
public void updatePackageAsync(String packageName) {
|
||||||
|
// Need at least first load, since we're going to be using previously
|
||||||
|
// cached values for non-matching packages.
|
||||||
|
waitForFirstLoad();
|
||||||
|
new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gather roots from storage providers belonging to given authority.
|
||||||
|
*/
|
||||||
|
public void updateAuthorityAsync(String authority) {
|
||||||
|
final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
|
||||||
|
if (info != null) {
|
||||||
|
updatePackageAsync(info.packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForFirstLoad() {
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
success = mFirstLoad.await(15, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
Log.w(TAG, "Timeout waiting for first update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load roots from authorities that are in stopped state. Normal
|
||||||
|
* {@link UpdateTask} passes ignore stopped applications.
|
||||||
|
*/
|
||||||
|
private void loadStoppedAuthorities() {
|
||||||
|
final ContentResolver resolver = mContext.getContentResolver();
|
||||||
|
synchronized (mLock) {
|
||||||
|
for (String authority : mStoppedAuthorities) {
|
||||||
|
if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
|
||||||
|
mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
|
||||||
|
}
|
||||||
|
mStoppedAuthorities.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpdateTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
private final String mFilterPackage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all roots.
|
||||||
|
*/
|
||||||
|
public UpdateTask() {
|
||||||
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for other storage backends
|
/**
|
||||||
final ContentResolver resolver = mContext.getContentResolver();
|
* Only update roots belonging to given package name. Other roots will
|
||||||
final PackageManager pm = mContext.getPackageManager();
|
* be copied from cached {@link #mRoots} values.
|
||||||
final List<ProviderInfo> providers = pm.queryContentProviders(
|
*/
|
||||||
null, -1, PackageManager.GET_META_DATA);
|
public UpdateTask(String filterPackage) {
|
||||||
for (ProviderInfo info : providers) {
|
mFilterPackage = filterPackage;
|
||||||
if (info.metaData != null && info.metaData.containsKey(
|
}
|
||||||
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
|
|
||||||
|
|
||||||
// TODO: populate roots on background thread, and cache results
|
@Override
|
||||||
final Uri rootsUri = DocumentsContract.buildRootsUri(info.authority);
|
protected Void doInBackground(Void... params) {
|
||||||
final ContentProviderClient client = resolver
|
final long start = SystemClock.elapsedRealtime();
|
||||||
.acquireUnstableContentProviderClient(info.authority);
|
|
||||||
Cursor cursor = null;
|
final Multimap<String, RootInfo> roots = ArrayListMultimap.create();
|
||||||
try {
|
final HashSet<String> stoppedAuthorities = Sets.newHashSet();
|
||||||
cursor = client.query(rootsUri, null, null, null, null);
|
|
||||||
while (cursor.moveToNext()) {
|
final ContentResolver resolver = mContext.getContentResolver();
|
||||||
final RootInfo root = RootInfo.fromRootsCursor(info.authority, cursor);
|
final PackageManager pm = mContext.getPackageManager();
|
||||||
mRoots.add(root);
|
final List<ProviderInfo> providers = pm.queryContentProviders(
|
||||||
|
null, -1, PackageManager.GET_META_DATA);
|
||||||
|
for (ProviderInfo info : providers) {
|
||||||
|
if (info.metaData != null && info.metaData.containsKey(
|
||||||
|
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
|
||||||
|
// Ignore stopped packages for now; we might query them
|
||||||
|
// later during UI interaction.
|
||||||
|
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
|
||||||
|
if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
|
||||||
|
stoppedAuthorities.add(info.authority);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try using cached roots if filtering
|
||||||
|
boolean cacheHit = false;
|
||||||
|
if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (roots.putAll(info.authority, mRoots.get(info.authority))) {
|
||||||
|
if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
|
||||||
|
cacheHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss, or loading everything
|
||||||
|
if (!cacheHit) {
|
||||||
|
roots.putAll(
|
||||||
|
info.authority, loadRootsForAuthority(resolver, info.authority));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e);
|
|
||||||
} finally {
|
|
||||||
IoUtils.closeQuietly(cursor);
|
|
||||||
ContentProviderClient.closeQuietly(client);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Update found " + mRoots.size() + " roots");
|
final long delta = SystemClock.elapsedRealtime() - start;
|
||||||
|
Log.d(TAG, "Update found " + roots.size() + " roots in " + delta + "ms");
|
||||||
|
synchronized (mLock) {
|
||||||
|
mStoppedAuthorities = stoppedAuthorities;
|
||||||
|
mRoots = roots;
|
||||||
|
}
|
||||||
|
mFirstLoad.countDown();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
/**
|
||||||
public RootInfo findRoot(Uri uri) {
|
* Bring up requested provider and query for all active roots.
|
||||||
final String authority = uri.getAuthority();
|
*/
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
|
||||||
for (RootInfo root : mRoots) {
|
if (LOGD) Log.d(TAG, "Loading roots for " + authority);
|
||||||
if (Objects.equal(root.authority, authority) && Objects.equal(root.documentId, docId)) {
|
|
||||||
|
synchronized (mObservedAuthorities) {
|
||||||
|
if (mObservedAuthorities.add(authority)) {
|
||||||
|
// Watch for any future updates
|
||||||
|
final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
|
||||||
|
mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<RootInfo> roots = Lists.newArrayList();
|
||||||
|
final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
|
||||||
|
final ContentProviderClient client = resolver
|
||||||
|
.acquireUnstableContentProviderClient(authority);
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = client.query(rootsUri, null, null, null, null);
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
|
||||||
|
roots.add(root);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
|
||||||
|
} finally {
|
||||||
|
IoUtils.closeQuietly(cursor);
|
||||||
|
ContentProviderClient.closeQuietly(client);
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the requested {@link RootInfo}, but only loading the roots for the
|
||||||
|
* requested authority. This is useful when we want to load fast without
|
||||||
|
* waiting for all the other roots to come back.
|
||||||
|
*/
|
||||||
|
public RootInfo getRootOneshot(String authority, String rootId) {
|
||||||
|
synchronized (mLock) {
|
||||||
|
RootInfo root = getRootLocked(authority, rootId);
|
||||||
|
if (root == null) {
|
||||||
|
mRoots.putAll(
|
||||||
|
authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
|
||||||
|
root = getRootLocked(authority, rootId);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootInfo getRootBlocking(String authority, String rootId) {
|
||||||
|
waitForFirstLoad();
|
||||||
|
loadStoppedAuthorities();
|
||||||
|
synchronized (mLock) {
|
||||||
|
return getRootLocked(authority, rootId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RootInfo getRootLocked(String authority, String rootId) {
|
||||||
|
for (RootInfo root : mRoots.get(authority)) {
|
||||||
|
if (Objects.equal(root.rootId, rootId)) {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
public boolean isIconUniqueBlocking(RootInfo root) {
|
||||||
public RootInfo getRoot(String authority, String rootId) {
|
waitForFirstLoad();
|
||||||
for (RootInfo root : mRoots) {
|
loadStoppedAuthorities();
|
||||||
if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) {
|
synchronized (mLock) {
|
||||||
return root;
|
final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
|
||||||
}
|
for (RootInfo test : mRoots.get(root.authority)) {
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
|
||||||
public boolean isIconUnique(RootInfo root) {
|
|
||||||
final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
|
|
||||||
for (RootInfo test : mRoots) {
|
|
||||||
if (Objects.equal(test.authority, root.authority)) {
|
|
||||||
if (Objects.equal(test.rootId, root.rootId)) {
|
if (Objects.equal(test.rootId, root.rootId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -148,32 +306,37 @@ public class RootsCache {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
|
||||||
public RootInfo getRecentsRoot() {
|
public RootInfo getRecentsRoot() {
|
||||||
return mRecentsRoot;
|
return mRecentsRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
|
||||||
public boolean isRecentsRoot(RootInfo root) {
|
public boolean isRecentsRoot(RootInfo root) {
|
||||||
return mRecentsRoot == root;
|
return mRecentsRoot == root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
public Collection<RootInfo> getRootsBlocking() {
|
||||||
public List<RootInfo> getRoots() {
|
waitForFirstLoad();
|
||||||
return mRoots;
|
loadStoppedAuthorities();
|
||||||
|
synchronized (mLock) {
|
||||||
|
return mRoots.values();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("ActivityThread")
|
public Collection<RootInfo> getMatchingRootsBlocking(State state) {
|
||||||
public List<RootInfo> getMatchingRoots(State state) {
|
waitForFirstLoad();
|
||||||
return getMatchingRoots(mRoots, state);
|
loadStoppedAuthorities();
|
||||||
|
synchronized (mLock) {
|
||||||
|
return getMatchingRoots(mRoots.values(), state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RootInfo> getMatchingRoots(List<RootInfo> roots, State state) {
|
@VisibleForTesting
|
||||||
ArrayList<RootInfo> matching = Lists.newArrayList();
|
static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
|
||||||
|
final List<RootInfo> matching = Lists.newArrayList();
|
||||||
for (RootInfo root : roots) {
|
for (RootInfo root : roots) {
|
||||||
final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
|
final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
|
||||||
final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
|
final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ package com.android.documentsui;
|
|||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.FragmentManager;
|
import android.app.FragmentManager;
|
||||||
import android.app.FragmentTransaction;
|
import android.app.FragmentTransaction;
|
||||||
|
import android.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.Loader;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -44,6 +46,7 @@ import com.android.documentsui.model.DocumentInfo;
|
|||||||
import com.android.documentsui.model.RootInfo;
|
import com.android.documentsui.model.RootInfo;
|
||||||
import com.android.internal.util.Objects;
|
import com.android.internal.util.Objects;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -55,6 +58,8 @@ public class RootsFragment extends Fragment {
|
|||||||
private ListView mList;
|
private ListView mList;
|
||||||
private SectionedRootsAdapter mAdapter;
|
private SectionedRootsAdapter mAdapter;
|
||||||
|
|
||||||
|
private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
|
||||||
|
|
||||||
private static final String EXTRA_INCLUDE_APPS = "includeApps";
|
private static final String EXTRA_INCLUDE_APPS = "includeApps";
|
||||||
|
|
||||||
public static void show(FragmentManager fm, Intent includeApps) {
|
public static void show(FragmentManager fm, Intent includeApps) {
|
||||||
@@ -87,25 +92,49 @@ public class RootsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
super.onResume();
|
super.onActivityCreated(savedInstanceState);
|
||||||
updateRootsAdapter();
|
|
||||||
|
final Context context = getActivity();
|
||||||
|
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
||||||
|
final State state = ((DocumentsActivity) context).getDisplayState();
|
||||||
|
|
||||||
|
mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
|
||||||
|
@Override
|
||||||
|
public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
|
||||||
|
return new RootsLoader(context, roots, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(
|
||||||
|
Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
|
||||||
|
if (!isAdded()) return;
|
||||||
|
|
||||||
|
final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
|
||||||
|
|
||||||
|
mAdapter = new SectionedRootsAdapter(context, result, includeApps);
|
||||||
|
mList.setAdapter(mAdapter);
|
||||||
|
|
||||||
|
onCurrentRootChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
|
||||||
|
mAdapter = null;
|
||||||
|
mList.setAdapter(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRootsAdapter() {
|
@Override
|
||||||
final Context context = getActivity();
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
final Context context = getActivity();
|
||||||
final State state = ((DocumentsActivity) context).getDisplayState();
|
final State state = ((DocumentsActivity) context).getDisplayState();
|
||||||
state.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(context);
|
state.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(context);
|
||||||
|
|
||||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
getLoaderManager().restartLoader(2, null, mCallbacks);
|
||||||
final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
|
|
||||||
final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
|
|
||||||
|
|
||||||
mAdapter = new SectionedRootsAdapter(context, matchingRoots, includeApps);
|
|
||||||
mList.setAdapter(mAdapter);
|
|
||||||
|
|
||||||
onCurrentRootChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onCurrentRootChanged() {
|
public void onCurrentRootChanged() {
|
||||||
@@ -229,7 +258,8 @@ public class RootsFragment extends Fragment {
|
|||||||
private final RootsAdapter mDevices;
|
private final RootsAdapter mDevices;
|
||||||
private final AppsAdapter mApps;
|
private final AppsAdapter mApps;
|
||||||
|
|
||||||
public SectionedRootsAdapter(Context context, List<RootInfo> roots, Intent includeApps) {
|
public SectionedRootsAdapter(
|
||||||
|
Context context, Collection<RootInfo> roots, Intent includeApps) {
|
||||||
mServices = new RootsAdapter(context);
|
mServices = new RootsAdapter(context);
|
||||||
mShortcuts = new RootsAdapter(context);
|
mShortcuts = new RootsAdapter(context);
|
||||||
mDevices = new RootsAdapter(context);
|
mDevices = new RootsAdapter(context);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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.content.AsyncTaskLoader;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.android.documentsui.DocumentsActivity.State;
|
||||||
|
import com.android.documentsui.model.RootInfo;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class RootsLoader extends AsyncTaskLoader<Collection<RootInfo>> {
|
||||||
|
private final RootsCache mRoots;
|
||||||
|
private final State mState;
|
||||||
|
|
||||||
|
private Collection<RootInfo> mResult;
|
||||||
|
|
||||||
|
public RootsLoader(Context context, RootsCache roots, State state) {
|
||||||
|
super(context);
|
||||||
|
mRoots = roots;
|
||||||
|
mState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Collection<RootInfo> loadInBackground() {
|
||||||
|
return mRoots.getMatchingRootsBlocking(mState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliverResult(Collection<RootInfo> result) {
|
||||||
|
if (isReset()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Collection<RootInfo> oldResult = mResult;
|
||||||
|
mResult = result;
|
||||||
|
|
||||||
|
if (isStarted()) {
|
||||||
|
super.deliverResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStartLoading() {
|
||||||
|
if (mResult != null) {
|
||||||
|
deliverResult(mResult);
|
||||||
|
}
|
||||||
|
if (takeContentChanged() || mResult == null) {
|
||||||
|
forceLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStopLoading() {
|
||||||
|
cancelLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onReset() {
|
||||||
|
super.onReset();
|
||||||
|
|
||||||
|
// Ensure the loader is stopped
|
||||||
|
onStopLoading();
|
||||||
|
|
||||||
|
mResult = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,7 +68,6 @@ public class SaveFragment extends Fragment {
|
|||||||
public View onCreateView(
|
public View onCreateView(
|
||||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
final Context context = inflater.getContext();
|
final Context context = inflater.getContext();
|
||||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
|
||||||
|
|
||||||
final View view = inflater.inflate(R.layout.fragment_save, container, false);
|
final View view = inflater.inflate(R.layout.fragment_save, container, false);
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public class RootInfo implements Durable, Parcelable {
|
|||||||
public String mimeTypes;
|
public String mimeTypes;
|
||||||
|
|
||||||
/** Derived fields that aren't persisted */
|
/** Derived fields that aren't persisted */
|
||||||
|
public String derivedPackageName;
|
||||||
public String[] derivedMimeTypes;
|
public String[] derivedMimeTypes;
|
||||||
public int derivedIcon;
|
public int derivedIcon;
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ public class RootInfo implements Durable, Parcelable {
|
|||||||
availableBytes = -1;
|
availableBytes = -1;
|
||||||
mimeTypes = null;
|
mimeTypes = null;
|
||||||
|
|
||||||
|
derivedPackageName = null;
|
||||||
derivedMimeTypes = null;
|
derivedMimeTypes = null;
|
||||||
derivedIcon = 0;
|
derivedIcon = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
package com.android.externalstorage;
|
package com.android.externalstorage;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
import android.content.res.AssetFileDescriptor;
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.MatrixCursor;
|
import android.database.MatrixCursor;
|
||||||
@@ -52,7 +54,10 @@ import java.lang.ref.WeakReference;
|
|||||||
public class TestDocumentsProvider extends DocumentsProvider {
|
public class TestDocumentsProvider extends DocumentsProvider {
|
||||||
private static final String TAG = "TestDocuments";
|
private static final String TAG = "TestDocuments";
|
||||||
|
|
||||||
|
private static final boolean LAG_ROOTS = false;
|
||||||
private static final boolean CRASH_ROOTS = false;
|
private static final boolean CRASH_ROOTS = false;
|
||||||
|
private static final boolean REFRESH_ROOTS = false;
|
||||||
|
|
||||||
private static final boolean CRASH_DOCUMENT = false;
|
private static final boolean CRASH_DOCUMENT = false;
|
||||||
|
|
||||||
private static final String MY_ROOT_ID = "myRoot";
|
private static final String MY_ROOT_ID = "myRoot";
|
||||||
@@ -78,17 +83,42 @@ public class TestDocumentsProvider extends DocumentsProvider {
|
|||||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String mAuthority;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attachInfo(Context context, ProviderInfo info) {
|
||||||
|
mAuthority = info.authority;
|
||||||
|
super.attachInfo(context, info);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||||
|
Log.d(TAG, "Someone asked for our roots!");
|
||||||
|
|
||||||
|
if (LAG_ROOTS) SystemClock.sleep(3000);
|
||||||
if (CRASH_ROOTS) System.exit(12);
|
if (CRASH_ROOTS) System.exit(12);
|
||||||
|
|
||||||
|
if (REFRESH_ROOTS) {
|
||||||
|
new AsyncTask<Void, Void, Void>() {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params) {
|
||||||
|
SystemClock.sleep(3000);
|
||||||
|
Log.d(TAG, "Notifying that something changed!!");
|
||||||
|
final Uri uri = DocumentsContract.buildRootsUri(mAuthority);
|
||||||
|
getContext().getContentResolver().notifyChange(uri, null, false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
|
||||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||||
final RowBuilder row = result.newRow();
|
final RowBuilder row = result.newRow();
|
||||||
row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
|
row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
|
||||||
row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE);
|
row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE);
|
||||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS);
|
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS);
|
||||||
row.add(Root.COLUMN_TITLE, "_Test title which is really long");
|
row.add(Root.COLUMN_TITLE, "_Test title which is really long");
|
||||||
row.add(Root.COLUMN_SUMMARY, "_Summary which is also super long text");
|
row.add(Root.COLUMN_SUMMARY,
|
||||||
|
SystemClock.elapsedRealtime() + " summary which is also super long text");
|
||||||
row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
|
row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, 1024);
|
row.add(Root.COLUMN_AVAILABLE_BYTES, 1024);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user