Merge "Several fixes to Fingerprint code after large merge - route fingerprint enrollment auth token - replace "processed" event with "authenticated" - fix type-o in strings.xml"
diff --git a/core/java/android/os/storage/DiskInfo.java b/core/java/android/os/storage/DiskInfo.java
index e6160aa..dc96640 100644
--- a/core/java/android/os/storage/DiskInfo.java
+++ b/core/java/android/os/storage/DiskInfo.java
@@ -16,6 +16,7 @@
package android.os.storage;
+import android.annotation.NonNull;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
@@ -59,6 +60,10 @@
volumeIds = parcel.readStringArray();
}
+ public @NonNull String getId() {
+ return id;
+ }
+
public String getDescription() {
// TODO: splice vendor label into these strings
if ((flags & FLAG_SD) != 0) {
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 10ffd48..0a8187e 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -923,12 +923,13 @@
}
@Override
- public VolumeInfo[] getVolumes() throws RemoteException {
+ public VolumeInfo[] getVolumes(int _flags) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
VolumeInfo[] _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(_flags);
mRemote.transact(Stub.TRANSACTION_getVolumes, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArray(VolumeInfo.CREATOR);
@@ -1029,6 +1030,39 @@
_data.recycle();
}
}
+
+ @Override
+ public void setVolumeNickname(String volId, String nickname) throws RemoteException {
+ Parcel _data = Parcel.obtain();
+ Parcel _reply = Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeString(volId);
+ _data.writeString(nickname);
+ mRemote.transact(Stub.TRANSACTION_setVolumeNickname, _data, _reply, 0);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+
+ @Override
+ public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException {
+ Parcel _data = Parcel.obtain();
+ Parcel _reply = Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeString(volId);
+ _data.writeInt(flags);
+ _data.writeInt(mask);
+ mRemote.transact(Stub.TRANSACTION_setVolumeUserFlags, _data, _reply, 0);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
}
private static final String DESCRIPTOR = "IMountService";
@@ -1132,6 +1166,9 @@
static final int TRANSACTION_partitionPrivate = IBinder.FIRST_CALL_TRANSACTION + 50;
static final int TRANSACTION_partitionMixed = IBinder.FIRST_CALL_TRANSACTION + 51;
+ static final int TRANSACTION_setVolumeNickname = IBinder.FIRST_CALL_TRANSACTION + 52;
+ static final int TRANSACTION_setVolumeUserFlags = IBinder.FIRST_CALL_TRANSACTION + 53;
+
/**
* Cast an IBinder object into an IMountService interface, generating a
* proxy if needed.
@@ -1566,7 +1603,8 @@
}
case TRANSACTION_getVolumes: {
data.enforceInterface(DESCRIPTOR);
- VolumeInfo[] volumes = getVolumes();
+ int _flags = data.readInt();
+ VolumeInfo[] volumes = getVolumes(_flags);
reply.writeNoException();
reply.writeTypedArray(volumes, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
return true;
@@ -1614,6 +1652,23 @@
reply.writeNoException();
return true;
}
+ case TRANSACTION_setVolumeNickname: {
+ data.enforceInterface(DESCRIPTOR);
+ String volId = data.readString();
+ String nickname = data.readString();
+ setVolumeNickname(volId, nickname);
+ reply.writeNoException();
+ return true;
+ }
+ case TRANSACTION_setVolumeUserFlags: {
+ data.enforceInterface(DESCRIPTOR);
+ String volId = data.readString();
+ int _flags = data.readInt();
+ int _mask = data.readInt();
+ setVolumeUserFlags(volId, _flags, _mask);
+ reply.writeNoException();
+ return true;
+ }
}
return super.onTransact(code, data, reply, flags);
}
@@ -1902,7 +1957,7 @@
public void waitForAsecScan() throws RemoteException;
public DiskInfo[] getDisks() throws RemoteException;
- public VolumeInfo[] getVolumes() throws RemoteException;
+ public VolumeInfo[] getVolumes(int flags) throws RemoteException;
public void mount(String volId) throws RemoteException;
public void unmount(String volId) throws RemoteException;
@@ -1911,4 +1966,7 @@
public void partitionPublic(String diskId) throws RemoteException;
public void partitionPrivate(String diskId) throws RemoteException;
public void partitionMixed(String diskId, int ratio) throws RemoteException;
+
+ public void setVolumeNickname(String volId, String nickname) throws RemoteException;
+ public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException;
}
diff --git a/core/java/android/os/storage/IMountServiceListener.java b/core/java/android/os/storage/IMountServiceListener.java
index 3965f9d..fd914bc 100644
--- a/core/java/android/os/storage/IMountServiceListener.java
+++ b/core/java/android/os/storage/IMountServiceListener.java
@@ -91,6 +91,13 @@
reply.writeNoException();
return true;
}
+ case TRANSACTION_onVolumeMetadataChanged: {
+ data.enforceInterface(DESCRIPTOR);
+ final VolumeInfo vol = (VolumeInfo) data.readParcelable(null);
+ onVolumeMetadataChanged(vol);
+ reply.writeNoException();
+ return true;
+ }
}
return super.onTransact(code, data, reply, flags);
}
@@ -175,6 +182,22 @@
_data.recycle();
}
}
+
+ @Override
+ public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException {
+ Parcel _data = Parcel.obtain();
+ Parcel _reply = Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeParcelable(vol, 0);
+ mRemote.transact(Stub.TRANSACTION_onVolumeMetadataChanged, _data, _reply,
+ android.os.IBinder.FLAG_ONEWAY);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
}
static final int TRANSACTION_onUsbMassStorageConnectionChanged = (IBinder.FIRST_CALL_TRANSACTION + 0);
@@ -182,6 +205,7 @@
static final int TRANSACTION_onStorageStateChanged = (IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_onVolumeStateChanged = (IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_onVolumeMetadataChanged = (IBinder.FIRST_CALL_TRANSACTION + 3);
}
/**
@@ -204,4 +228,6 @@
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState)
throws RemoteException;
+
+ public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException;
}
diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java
index 29d5494..28a187d 100644
--- a/core/java/android/os/storage/StorageEventListener.java
+++ b/core/java/android/os/storage/StorageEventListener.java
@@ -40,4 +40,7 @@
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
}
+
+ public void onVolumeMetadataChanged(VolumeInfo vol) {
+ }
}
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index eb77477..0e977ff 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -70,6 +70,9 @@
/** {@hide} */
public static final String PROP_PRIMARY_PHYSICAL = "ro.vold.primary_physical";
+ /** {@hide} */
+ public static final int FLAG_ALL_METADATA = 1 << 0;
+
private final Context mContext;
private final ContentResolver mResolver;
@@ -83,6 +86,7 @@
Handler.Callback {
private static final int MSG_STORAGE_STATE_CHANGED = 1;
private static final int MSG_VOLUME_STATE_CHANGED = 2;
+ private static final int MSG_VOLUME_METADATA_CHANGED = 3;
final StorageEventListener mCallback;
final Handler mHandler;
@@ -105,6 +109,10 @@
mCallback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
args.recycle();
return true;
+ case MSG_VOLUME_METADATA_CHANGED:
+ mCallback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+ args.recycle();
+ return true;
}
args.recycle();
return false;
@@ -132,6 +140,13 @@
args.argi3 = newState;
mHandler.obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
}
+
+ @Override
+ public void onVolumeMetadataChanged(VolumeInfo vol) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = vol;
+ mHandler.obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
+ }
}
/**
@@ -480,8 +495,13 @@
/** {@hide} */
public @NonNull List<VolumeInfo> getVolumes() {
+ return getVolumes(0);
+ }
+
+ /** {@hide} */
+ public @NonNull List<VolumeInfo> getVolumes(int flags) {
try {
- return Arrays.asList(mMountService.getVolumes());
+ return Arrays.asList(mMountService.getVolumes(flags));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
@@ -556,6 +576,35 @@
}
/** {@hide} */
+ public void setVolumeNickname(String volId, String nickname) {
+ try {
+ mMountService.setVolumeNickname(volId, nickname);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /** {@hide} */
+ public void setVolumeInited(String volId, boolean inited) {
+ try {
+ mMountService.setVolumeUserFlags(volId, inited ? VolumeInfo.USER_FLAG_INITED : 0,
+ VolumeInfo.USER_FLAG_INITED);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /** {@hide} */
+ public void setVolumeSnoozed(String volId, boolean snoozed) {
+ try {
+ mMountService.setVolumeUserFlags(volId, snoozed ? VolumeInfo.USER_FLAG_SNOOZED : 0,
+ VolumeInfo.USER_FLAG_SNOOZED);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /** {@hide} */
public @Nullable StorageVolume getStorageVolume(File file) {
return getStorageVolume(getVolumeList(), file);
}
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index fe1e206..f06fc8c 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -71,6 +71,9 @@
public static final int FLAG_PRIMARY = 1 << 0;
public static final int FLAG_VISIBLE = 1 << 1;
+ public static final int USER_FLAG_INITED = 1 << 0;
+ public static final int USER_FLAG_SNOOZED = 1 << 1;
+
private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
@@ -104,8 +107,9 @@
/** Framework state */
public final int mtpIndex;
- public String nickname;
public String diskId;
+ public String nickname;
+ public int userFlags = 0;
public VolumeInfo(String id, int type, int mtpIndex) {
this.id = Preconditions.checkNotNull(id);
@@ -124,8 +128,9 @@
fsLabel = parcel.readString();
path = parcel.readString();
mtpIndex = parcel.readInt();
- nickname = parcel.readString();
diskId = parcel.readString();
+ nickname = parcel.readString();
+ userFlags = parcel.readInt();
}
public static @NonNull String getEnvironmentForState(int state) {
@@ -145,6 +150,30 @@
return getBroadcastForEnvironment(getEnvironmentForState(state));
}
+ public @NonNull String getId() {
+ return id;
+ }
+
+ public @Nullable String getDiskId() {
+ return diskId;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public @Nullable String getFsUuid() {
+ return fsUuid;
+ }
+
+ public @Nullable String getNickname() {
+ return nickname;
+ }
+
public @Nullable String getDescription() {
if (ID_PRIVATE_INTERNAL.equals(id)) {
return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
@@ -165,6 +194,14 @@
return (flags & FLAG_VISIBLE) != 0;
}
+ public boolean isInited() {
+ return (userFlags & USER_FLAG_INITED) != 0;
+ }
+
+ public boolean isSnoozed() {
+ return (userFlags & USER_FLAG_SNOOZED) != 0;
+ }
+
public boolean isVisibleToUser(int userId) {
if (type == TYPE_PUBLIC && userId == this.userId) {
return isVisible();
@@ -175,6 +212,10 @@
}
}
+ public File getPath() {
+ return new File(path);
+ }
+
public File getPathForUser(int userId) {
if (path == null) {
return null;
@@ -284,8 +325,9 @@
pw.println();
pw.printPair("path", path);
pw.printPair("mtpIndex", mtpIndex);
- pw.printPair("nickname", nickname);
pw.printPair("diskId", diskId);
+ pw.printPair("nickname", nickname);
+ pw.printPair("userFlags", DebugUtils.flagsToString(getClass(), "USER_FLAG_", userFlags));
pw.decreaseIndent();
pw.println();
}
@@ -331,7 +373,8 @@
parcel.writeString(fsLabel);
parcel.writeString(path);
parcel.writeInt(mtpIndex);
- parcel.writeString(nickname);
parcel.writeString(diskId);
+ parcel.writeString(nickname);
+ parcel.writeInt(userFlags);
}
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 2bcb352..7828851 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -811,7 +811,7 @@
float sum = 0;
int i;
- for (i = len; i >= 0; i--) {
+ for (i = len; i > 0; i--) {
float w = widths[i - 1 + lineStart - widthStart];
if (w + sum + ellipsisWidth > avail) {
diff --git a/packages/DocumentsUI/res/layout/fragment_pick.xml b/packages/DocumentsUI/res/layout/fragment_pick.xml
index 5735871..87dc4f8 100644
--- a/packages/DocumentsUI/res/layout/fragment_pick.xml
+++ b/packages/DocumentsUI/res/layout/fragment_pick.xml
@@ -21,12 +21,19 @@
android:baselineAligned="false"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall">
-
+ <Button
+ android:id="@android:id/button2"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="@android:string/cancel"
+ android:visibility="gone"
+ style="?android:attr/buttonBarNegativeButtonStyle" />
<Button
android:id="@android:id/button1"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:layout_weight="1"
android:textAllCaps="false"
style="?android:attr/buttonBarPositiveButtonStyle" />
-
</LinearLayout>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 3ca239a..062d433 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -65,6 +65,9 @@
<!-- Menu item that hides the sizes of displayed files [CHAR LIMIT=24] -->
<string name="menu_file_size_hide">Hide file size</string>
+ <!-- Button label that copies files to the current directory [CHAR LIMIT=48] -->
+ <string name="button_copy">Copy</string>
+
<!-- Action mode title summarizing the number of documents selected [CHAR LIMIT=32] -->
<string name="mode_selected_count"><xliff:g id="count" example="3">%1$d</xliff:g> selected</string>
@@ -112,8 +115,6 @@
<!-- Title of dialog when prompting user to select an app to share documents with [CHAR LIMIT=32] -->
<string name="share_via">Share via</string>
- <!-- Title of the cancel button [CHAR LIMIT=24] -->
- <string name="cancel">Cancel</string>
<!-- Title of the copy notification [CHAR LIMIT=24] -->
<string name="copy_notification_title">Copying files</string>
<!-- Text shown on the copy notification to indicate remaining time, in minutes [CHAR LIMIT=24] -->
@@ -125,5 +126,17 @@
</plurals>
<!-- Text shown on the copy notification while DocumentsUI performs setup in preparation for copying files [CHAR LIMIT=32] -->
<string name="copy_preparing">Preparing for copy\u2026</string>
-
+ <!-- Title of the copy error notification [CHAR LIMIT=48] -->
+ <plurals name="copy_error_notification_title">
+ <item quantity="one">Error copying <xliff:g id="count" example="1">%1$d</xliff:g> file.</item>
+ <item quantity="other">Error copying <xliff:g id="count" example="1">%1$d</xliff:g> files.</item>
+ </plurals>
+ <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
+ <string name="notification_touch_for_details">Touch to view details</string>
+ <!-- Label of a dialog button for retrying a failed operation [CHAR LIMIT=24] -->
+ <string name="retry">Retry</string>
+ <!-- Title of the copying failure alert dialog. [CHAR LIMIT=48] -->
+ <string name="copy_failure_alert_title">Error copying files</string>
+ <!-- Contents of the copying failure alert dialog. [CHAR LIMIT=48] -->
+ <string name="copy_failure_alert_content">Following files are not copied: <xliff:g id="list">%1$s</xliff:g></string>
</resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index bd17401..66792da 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -33,10 +33,6 @@
import com.google.common.collect.Maps;
abstract class BaseActivity extends Activity {
- /** Intent action name to open copy destination. */
- public static String ACTION_OPEN_COPY_DESTINATION_STRING =
- "com.android.documentsui.OPEN_COPY_DESTINATION";
-
public abstract State getDisplayState();
public abstract RootInfo getCurrentRoot();
public abstract void onStateChanged();
@@ -56,6 +52,18 @@
return (BaseActivity) fragment.getActivity();
}
+ public static abstract class DocumentsIntent {
+ /** Intent action name to open copy destination. */
+ public static String ACTION_OPEN_COPY_DESTINATION =
+ "com.android.documentsui.OPEN_COPY_DESTINATION";
+
+ /**
+ * Extra boolean flag for ACTION_OPEN_COPY_DESTINATION_STRING, which
+ * specifies if the destination directory needs to create new directory or not.
+ */
+ public static String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY";
+ }
+
public static class State implements android.os.Parcelable {
public int action;
public String[] acceptMimes;
@@ -77,6 +85,7 @@
public boolean showAdvanced = false;
public boolean stackTouched = false;
public boolean restored = false;
+ public boolean directoryCopy = false;
/** Current user navigation stack; empty implies recents. */
public DocumentStack stack = new DocumentStack();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
index e140f33..337b862 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
@@ -57,6 +57,10 @@
private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
public static final String EXTRA_STACK = "com.android.documentsui.STACK";
+ public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE";
+
+ // TODO: Move it to a shared file when more operations are implemented.
+ public static final int FAILURE_COPY = 1;
private NotificationManager mNotificationManager;
private Notification.Builder mProgressBuilder;
@@ -66,7 +70,7 @@
private volatile boolean mIsCancelled;
// Parameters of the copy job. Requests to an IntentService are serialized so this code only
// needs to deal with one job at a time.
- private final List<Uri> mFailedFiles;
+ private final ArrayList<Uri> mFailedFiles;
private long mBatchSize;
private long mBytesCopied;
private long mStartTime;
@@ -128,7 +132,23 @@
mNotificationManager.cancel(mJobId, 0);
if (mFailedFiles.size() > 0) {
- // TODO: Display a notification when an error has occurred.
+ final Context context = getApplicationContext();
+ final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
+ navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
+ navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
+ navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, mFailedFiles);
+
+ final Notification.Builder errorBuilder = new Notification.Builder(this)
+ .setContentTitle(context.getResources().
+ getQuantityString(R.plurals.copy_error_notification_title,
+ mFailedFiles.size(), mFailedFiles.size()))
+ .setContentText(getString(R.string.notification_touch_for_details))
+ .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setSmallIcon(R.drawable.ic_menu_copy)
+ .setAutoCancel(true);
+ mNotificationManager.notify(mJobId, 0, errorBuilder.build());
}
// TODO: Display a toast if the copy was cancelled.
@@ -158,13 +178,14 @@
final Context context = getApplicationContext();
final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
- navigateIntent.putExtra(EXTRA_STACK, (Parcelable)stack);
+ navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
mProgressBuilder = new Notification.Builder(this)
.setContentTitle(getString(R.string.copy_notification_title))
.setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 0))
.setCategory(Notification.CATEGORY_PROGRESS)
- .setSmallIcon(R.drawable.ic_menu_copy).setOngoing(true);
+ .setSmallIcon(R.drawable.ic_menu_copy)
+ .setOngoing(true);
final Intent cancelIntent = new Intent(this, CopyService.class);
cancelIntent.putExtra(EXTRA_CANCEL, mJobId);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 61fcad2..e2e9807 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -683,10 +683,18 @@
// Pop up a dialog to pick a destination. This is inadequate but works for now.
// TODO: Implement a picker that is to spec.
final Intent intent = new Intent(
- BaseActivity.ACTION_OPEN_COPY_DESTINATION_STRING,
+ BaseActivity.DocumentsIntent.ACTION_OPEN_COPY_DESTINATION,
Uri.EMPTY,
getActivity(),
DocumentsActivity.class);
+ boolean directoryCopy = false;
+ for (DocumentInfo info : docs) {
+ if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
+ directoryCopy = true;
+ break;
+ }
+ }
+ intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
startActivityForResult(intent, REQUEST_COPY_DESTINATION);
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 1e798eb..a2a789f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -237,7 +237,7 @@
mState.action = ACTION_MANAGE;
} else if (DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT.equals(action)) {
mState.action = ACTION_BROWSE;
- } else if (ACTION_OPEN_COPY_DESTINATION_STRING.equals(action)) {
+ } else if (DocumentsIntent.ACTION_OPEN_COPY_DESTINATION.equals(action)) {
mState.action = ACTION_OPEN_COPY_DESTINATION;
}
@@ -265,6 +265,10 @@
} else {
mState.showSize = LocalPreferences.getDisplayFileSize(this);
}
+ if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
+ mState.directoryCopy = intent.getBooleanExtra(
+ BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, false);
+ }
}
private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
@@ -906,7 +910,7 @@
if (pick != null) {
final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
: cwd.displayName;
- pick.setPickTarget(cwd, displayName);
+ pick.setPickTarget(mState.action, cwd, displayName);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java
new file mode 100644
index 0000000..1748c9c
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 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.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+
+import com.android.documentsui.model.DocumentInfo;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
+/**
+ * Alert dialog for failed operations.
+ */
+public class FailureDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener {
+ private static final String TAG = "FailureDialogFragment";
+
+ private int mFailure;
+ private ArrayList<Uri> mFailedSrcList;
+
+ public static void show(FragmentManager fm, int failure, ArrayList<Uri> failedSrcList) {
+ // TODO: Add support for other failures than copy.
+ if (failure != CopyService.FAILURE_COPY) {
+ return;
+ }
+
+ final Bundle args = new Bundle();
+ args.putInt(CopyService.EXTRA_FAILURE, failure);
+ args.putParcelableArrayList(CopyService.EXTRA_SRC_LIST, failedSrcList);
+
+ final FragmentTransaction ft = fm.beginTransaction();
+ final FailureDialogFragment fragment = new FailureDialogFragment();
+ fragment.setArguments(args);
+
+ ft.add(fragment, TAG);
+ ft.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // TODO: Pass mFailure and mFailedSrcList to the parent fragment.
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle inState) {
+ super.onCreate(inState);
+
+ mFailure = getArguments().getInt(CopyService.EXTRA_FAILURE);
+ mFailedSrcList = getArguments().getParcelableArrayList(CopyService.EXTRA_SRC_LIST);
+
+ final StringBuilder list = new StringBuilder("<p>");
+ for (Uri documentUri : mFailedSrcList) {
+ try {
+ final DocumentInfo documentInfo = DocumentInfo.fromUri(
+ getActivity().getContentResolver(), documentUri);
+ list.append(String.format("• %s<br>", documentInfo.displayName));
+ }
+ catch (FileNotFoundException ignore) {
+ // Source file most probably gone.
+ }
+ }
+ list.append("</p>");
+ final String message = String.format(getString(R.string.copy_failure_alert_content),
+ list.toString());
+
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.copy_failure_alert_title))
+ .setMessage(Html.fromHtml(message))
+ // TODO: Implement retrying the copy operation.
+ .setPositiveButton(R.string.retry, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .create();
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
index 4b008ca..5e565bf 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import android.R.string;
+import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
@@ -40,6 +42,7 @@
private View mContainer;
private Button mPick;
+ private Button mCancel;
public static void show(FragmentManager fm) {
final PickFragment fragment = new PickFragment();
@@ -61,7 +64,10 @@
mPick = (Button) mContainer.findViewById(android.R.id.button1);
mPick.setOnClickListener(mPickListener);
- setPickTarget(null, null);
+ mCancel = (Button) mContainer.findViewById(android.R.id.button2);
+ mCancel.setOnClickListener(mCancelListener);
+
+ setPickTarget(0, null, null);
return mContainer;
}
@@ -74,18 +80,43 @@
}
};
- public void setPickTarget(DocumentInfo pickTarget, CharSequence displayName) {
- mPickTarget = pickTarget;
+ private View.OnClickListener mCancelListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final BaseActivity activity = BaseActivity.get(PickFragment.this);
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ }
+ };
+ /**
+ * @param action Which action defined in BaseActivity.State is the picker shown for.
+ */
+ public void setPickTarget(int action,
+ DocumentInfo pickTarget,
+ CharSequence displayName) {
if (mContainer != null) {
- if (mPickTarget != null) {
+ if (pickTarget != null) {
mContainer.setVisibility(View.VISIBLE);
final Locale locale = getResources().getConfiguration().locale;
- final String raw = getString(R.string.menu_select).toUpperCase(locale);
- mPick.setText(TextUtils.expandTemplate(raw, displayName));
+ switch (action) {
+ case BaseActivity.State.ACTION_OPEN_TREE:
+ final String raw = getString(R.string.menu_select).toUpperCase(locale);
+ mPick.setText(TextUtils.expandTemplate(raw, displayName));
+ mCancel.setVisibility(View.GONE);
+ break;
+ case BaseActivity.State.ACTION_OPEN_COPY_DESTINATION:
+ mPick.setText(getString(R.string.button_copy).toUpperCase(locale));
+ mCancel.setVisibility(View.VISIBLE);
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal action for PickFragment.");
+ }
+
} else {
mContainer.setVisibility(View.GONE);
}
}
+ mPickTarget = pickTarget;
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index d2267b1..27e8f20 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -367,6 +367,9 @@
if (!state.showAdvanced && advanced) continue;
// Exclude non-local devices when local only
if (state.localOnly && !localOnly) continue;
+ // Exclude downloads roots that don't support directory creation
+ // TODO: Add flag to check the root supports directory creation or not.
+ if (state.directoryCopy && root.isDownloads()) continue;
// Only show empty roots when creating
if ((state.action != State.ACTION_CREATE ||
state.action != State.ACTION_OPEN_TREE ||
diff --git a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
index f542838..976f21d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
@@ -63,6 +63,7 @@
import android.widget.Toast;
import android.widget.Toolbar;
+import com.android.documentsui.FailureDialogFragment;
import com.android.documentsui.RecentsProvider.ResumeColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
@@ -73,6 +74,7 @@
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -153,6 +155,13 @@
RootsFragment.show(getFragmentManager(), null);
if (!mState.restored) {
new RestoreStackTask().execute();
+ final Intent intent = getIntent();
+ final int failure = intent.getIntExtra(CopyService.EXTRA_FAILURE, 0);
+ if (failure != 0) {
+ final ArrayList<Uri> failedSrcList = intent.getParcelableArrayListExtra(
+ CopyService.EXTRA_SRC_LIST);
+ FailureDialogFragment.show(getFragmentManager(), failure, failedSrcList);
+ }
} else {
onCurrentDirectoryChanged(ANIM_NONE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index 6830957..818f5ee 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -20,7 +20,10 @@
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.os.UserHandle;
import android.os.storage.DiskInfo;
import android.os.storage.StorageEventListener;
@@ -38,6 +41,8 @@
private static final int NOTIF_ID = 0x53544f52; // STOR
+ private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME";
+
// TODO: delay some notifications to avoid bumpy fast operations
// TODO: annoy user when private media is missing
@@ -49,6 +54,25 @@
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
onVolumeStateChangedInternal(vol, oldState, newState);
}
+
+ @Override
+ public void onVolumeMetadataChanged(VolumeInfo vol) {
+ // Avoid kicking notifications when getting early metadata before
+ // mounted. If already mounted, we're being kicked because of a
+ // nickname or init'ed change.
+ if (vol.getState() == VolumeInfo.STATE_MOUNTED) {
+ onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
+ }
+ }
+ };
+
+ private final BroadcastReceiver mSnoozeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // TODO: kick this onto background thread
+ final String volId = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
+ mStorageManager.setVolumeSnoozed(volId, true);
+ }
};
@Override
@@ -58,23 +82,26 @@
mStorageManager = mContext.getSystemService(StorageManager.class);
mStorageManager.registerListener(mListener);
+ mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME),
+ android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null);
+
// Kick current state into place
final List<VolumeInfo> vols = mStorageManager.getVolumes();
for (VolumeInfo vol : vols) {
- onVolumeStateChangedInternal(vol, vol.state, vol.state);
+ onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
}
}
public void onVolumeStateChangedInternal(VolumeInfo vol, int oldState, int newState) {
// We only care about public volumes
- if (vol.type != VolumeInfo.TYPE_PUBLIC) {
+ if (vol.getType() != VolumeInfo.TYPE_PUBLIC) {
return;
}
Log.d(TAG, vol.toString());
// New state means we tear down any old notifications
- mNotificationManager.cancelAsUser(vol.id, NOTIF_ID, UserHandle.ALL);
+ mNotificationManager.cancelAsUser(vol.getId(), NOTIF_ID, UserHandle.ALL);
switch (newState) {
case VolumeInfo.STATE_UNMOUNTED:
@@ -106,7 +133,7 @@
}
private void onVolumeMounting(VolumeInfo vol) {
- final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+ final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
final CharSequence title = mContext.getString(
R.string.ext_media_checking_notification_title, disk.getDescription());
final CharSequence text = mContext.getString(
@@ -119,13 +146,16 @@
.setOngoing(true)
.build();
- mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+ mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
}
private void onVolumeMounted(VolumeInfo vol) {
- final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+ // Don't annoy when user dismissed in past
+ if (vol.isSnoozed()) return;
+
+ final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
final Notification notif;
- if (disk.isAdoptable()) {
+ if (disk.isAdoptable() && !vol.isInited()) {
final CharSequence title = disk.getDescription();
final CharSequence text = mContext.getString(
R.string.ext_media_new_notification_message, disk.getDescription());
@@ -136,6 +166,7 @@
buildInitPendingIntent(vol)))
.addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
buildUnmountPendingIntent(vol)))
+ .setDeleteIntent(buildSnoozeIntent(vol))
.setCategory(Notification.CATEGORY_SYSTEM)
.build();
@@ -150,12 +181,13 @@
buildBrowsePendingIntent(vol)))
.addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
buildUnmountPendingIntent(vol)))
+ .setDeleteIntent(buildSnoozeIntent(vol))
.setCategory(Notification.CATEGORY_SYSTEM)
.setPriority(Notification.PRIORITY_LOW)
.build();
}
- mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+ mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
}
private void onVolumeFormatting(VolumeInfo vol) {
@@ -163,7 +195,7 @@
}
private void onVolumeUnmounting(VolumeInfo vol) {
- final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+ final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
final CharSequence title = mContext.getString(
R.string.ext_media_unmounting_notification_title, disk.getDescription());
final CharSequence text = mContext.getString(
@@ -176,11 +208,11 @@
.setOngoing(true)
.build();
- mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+ mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
}
private void onVolumeUnmountable(VolumeInfo vol) {
- final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+ final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
final CharSequence title = mContext.getString(
R.string.ext_media_unmountable_notification_title, disk.getDescription());
final CharSequence text = mContext.getString(
@@ -192,7 +224,7 @@
.setCategory(Notification.CATEGORY_ERROR)
.build();
- mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+ mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
}
private void onVolumeRemoved(VolumeInfo vol) {
@@ -201,7 +233,7 @@
return;
}
- final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+ final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
final CharSequence title = mContext.getString(
R.string.ext_media_nomedia_notification_title, disk.getDescription());
final CharSequence text = mContext.getString(
@@ -212,7 +244,7 @@
.setCategory(Notification.CATEGORY_ERROR)
.build();
- mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+ mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
}
private Notification.Builder buildNotificationBuilder(CharSequence title, CharSequence text) {
@@ -229,28 +261,49 @@
final Intent intent = new Intent();
intent.setClassName("com.android.settings",
"com.android.settings.deviceinfo.StorageWizardInit");
- intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
- return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+ final int requestKey = vol.getId().hashCode();
+ return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
}
private PendingIntent buildUnmountPendingIntent(VolumeInfo vol) {
final Intent intent = new Intent();
intent.setClassName("com.android.settings",
"com.android.settings.deviceinfo.StorageUnmountReceiver");
- intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
- return PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, UserHandle.CURRENT);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+ final int requestKey = vol.getId().hashCode();
+ return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
}
private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) {
final Intent intent = vol.buildBrowseIntent();
- return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+
+ final int requestKey = vol.getId().hashCode();
+ return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
}
private PendingIntent buildDetailsPendingIntent(VolumeInfo vol) {
final Intent intent = new Intent();
intent.setClassName("com.android.settings",
"com.android.settings.Settings$StorageVolumeSettingsActivity");
- intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
- return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+ final int requestKey = vol.getId().hashCode();
+ return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+ }
+
+ private PendingIntent buildSnoozeIntent(VolumeInfo vol) {
+ final Intent intent = new Intent(ACTION_SNOOZE_VOLUME);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+ final int requestKey = vol.getId().hashCode();
+ return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
}
}
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index a99f387..4c937f7 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -16,6 +16,13 @@
package com.android.server;
+import static com.android.internal.util.XmlUtils.readIntAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.writeIntAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+
import android.Manifest;
import android.app.ActivityManagerNative;
import android.app.AppOpsManager;
@@ -55,9 +62,13 @@
import android.os.storage.VolumeInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.DebugUtils;
import android.util.Log;
import android.util.Slog;
+import android.util.Xml;
+import libcore.io.IoUtils;
import libcore.util.EmptyArray;
import libcore.util.HexEncoding;
@@ -66,14 +77,21 @@
import com.android.internal.app.IMediaContainerService;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.server.NativeDaemonConnector.Command;
import com.android.server.NativeDaemonConnector.SensitiveArg;
import com.android.server.pm.PackageManagerService;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
@@ -214,6 +232,57 @@
public static final int FstrimCompleted = 700;
}
+ private static final String TAG_VOLUMES = "volumes";
+ private static final String TAG_VOLUME = "volume";
+ private static final String ATTR_TYPE = "type";
+ private static final String ATTR_FS_UUID = "fsUuid";
+ private static final String ATTR_NICKNAME = "nickname";
+ private static final String ATTR_USER_FLAGS = "userFlags";
+
+ private final AtomicFile mMetadataFile;
+
+ private static class VolumeMetadata {
+ public final int type;
+ public final String fsUuid;
+ public String nickname;
+ public int userFlags;
+
+ public VolumeMetadata(int type, String fsUuid) {
+ this.type = type;
+ this.fsUuid = Preconditions.checkNotNull(fsUuid);
+ }
+
+ public static VolumeMetadata read(XmlPullParser in) throws IOException {
+ final int type = readIntAttribute(in, ATTR_TYPE);
+ final String fsUuid = readStringAttribute(in, ATTR_FS_UUID);
+ final VolumeMetadata meta = new VolumeMetadata(type, fsUuid);
+ meta.nickname = readStringAttribute(in, ATTR_NICKNAME);
+ meta.userFlags = readIntAttribute(in, ATTR_USER_FLAGS);
+ return meta;
+ }
+
+ public static void write(XmlSerializer out, VolumeMetadata meta) throws IOException {
+ out.startTag(null, TAG_VOLUME);
+ writeIntAttribute(out, ATTR_TYPE, meta.type);
+ writeStringAttribute(out, ATTR_FS_UUID, meta.fsUuid);
+ writeStringAttribute(out, ATTR_NICKNAME, meta.nickname);
+ writeIntAttribute(out, ATTR_USER_FLAGS, meta.userFlags);
+ out.endTag(null, TAG_VOLUME);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("VolumeMetadata:");
+ pw.increaseIndent();
+ pw.printPair("type", DebugUtils.valueToString(VolumeInfo.class, "TYPE_", type));
+ pw.printPair("fsUuid", fsUuid);
+ pw.printPair("nickname", nickname);
+ pw.printPair("userFlags",
+ DebugUtils.flagsToString(VolumeInfo.class, "USER_FLAG_", userFlags));
+ pw.decreaseIndent();
+ pw.println();
+ }
+ }
+
/**
* <em>Never</em> hold the lock while performing downcalls into vold, since
* unsolicited events can suddenly appear to update data structures.
@@ -222,11 +291,18 @@
@GuardedBy("mLock")
private int[] mStartedUsers = EmptyArray.INT;
+
+ /** Map from disk ID to disk */
@GuardedBy("mLock")
private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>();
+ /** Map from volume ID to disk */
@GuardedBy("mLock")
private ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>();
+ /** Map from UUID to metadata */
+ @GuardedBy("mLock")
+ private ArrayMap<String, VolumeMetadata> mMetadata = new ArrayMap<>();
+
private DiskInfo findDiskById(String id) {
synchronized (mLock) {
final DiskInfo disk = mDisks.get(id);
@@ -260,6 +336,15 @@
throw new IllegalArgumentException("No volume found for path " + path);
}
+ private VolumeMetadata findOrCreateMetadataLocked(VolumeInfo vol) {
+ VolumeMetadata meta = mMetadata.get(vol.fsUuid);
+ if (meta == null) {
+ meta = new VolumeMetadata(vol.type, vol.fsUuid);
+ mMetadata.put(meta.fsUuid, meta);
+ }
+ return meta;
+ }
+
private static int sNextMtpIndex = 1;
private static int allocateMtpIndex(String volId) {
@@ -799,6 +884,7 @@
if (vol != null) {
vol.fsType = cooked[2];
}
+ mCallbacks.notifyVolumeMetadataChanged(vol.clone());
break;
}
case VoldResponseCode.VOLUME_FS_UUID_CHANGED: {
@@ -807,6 +893,8 @@
if (vol != null) {
vol.fsUuid = cooked[2];
}
+ refreshMetadataLocked();
+ mCallbacks.notifyVolumeMetadataChanged(vol.clone());
break;
}
case VoldResponseCode.VOLUME_FS_LABEL_CHANGED: {
@@ -815,6 +903,7 @@
if (vol != null) {
vol.fsLabel = cooked[2];
}
+ mCallbacks.notifyVolumeMetadataChanged(vol.clone());
break;
}
case VoldResponseCode.VOLUME_PATH_CHANGED: {
@@ -901,6 +990,25 @@
}
}
+ /**
+ * Refresh latest metadata into any currently active {@link VolumeInfo}.
+ */
+ private void refreshMetadataLocked() {
+ final int size = mVolumes.size();
+ for (int i = 0; i < size; i++) {
+ final VolumeInfo vol = mVolumes.valueAt(i);
+ final VolumeMetadata meta = mMetadata.get(vol.fsUuid);
+
+ if (meta != null) {
+ vol.nickname = meta.nickname;
+ vol.userFlags = meta.userFlags;
+ } else {
+ vol.nickname = null;
+ vol.userFlags = 0;
+ }
+ }
+ }
+
private void enforcePermission(String perm) {
mContext.enforceCallingOrSelfPermission(perm, perm);
}
@@ -949,6 +1057,13 @@
mLastMaintenance = mLastMaintenanceFile.lastModified();
}
+ mMetadataFile = new AtomicFile(
+ new File(Environment.getSystemSecureDirectory(), "storage.xml"));
+
+ synchronized (mLock) {
+ readMetadataLocked();
+ }
+
/*
* Create the connection to vold with a maximum queue of twice the
* amount of containers we'd ever expect to have. This keeps an
@@ -972,6 +1087,61 @@
mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();
}
+ private void readMetadataLocked() {
+ mMetadata.clear();
+
+ FileInputStream fis = null;
+ try {
+ fis = mMetadataFile.openRead();
+ final XmlPullParser in = Xml.newPullParser();
+ in.setInput(fis, null);
+
+ int type;
+ while ((type = in.next()) != END_DOCUMENT) {
+ if (type == START_TAG) {
+ final String tag = in.getName();
+ if (TAG_VOLUME.equals(tag)) {
+ final VolumeMetadata meta = VolumeMetadata.read(in);
+ mMetadata.put(meta.fsUuid, meta);
+ }
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // Missing metadata is okay, probably first boot
+ } catch (IOException e) {
+ Slog.wtf(TAG, "Failed reading metadata", e);
+ } catch (XmlPullParserException e) {
+ Slog.wtf(TAG, "Failed reading metadata", e);
+ } finally {
+ IoUtils.closeQuietly(fis);
+ }
+ }
+
+ private void writeMetadataLocked() {
+ FileOutputStream fos = null;
+ try {
+ fos = mMetadataFile.startWrite();
+
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, "utf-8");
+ out.startDocument(null, true);
+ out.startTag(null, TAG_VOLUMES);
+ final int size = mMetadata.size();
+ for (int i = 0; i < size; i++) {
+ final VolumeMetadata meta = mMetadata.valueAt(i);
+ VolumeMetadata.write(out, meta);
+ }
+ out.endTag(null, TAG_VOLUMES);
+ out.endDocument();
+
+ mMetadataFile.finishWrite(fos);
+ } catch (IOException e) {
+ if (fos != null) {
+ mMetadataFile.failWrite(fos);
+ }
+ }
+ }
+
/**
* Exposed API calls below here
*/
@@ -1126,6 +1296,36 @@
}
@Override
+ public void setVolumeNickname(String volId, String nickname) {
+ enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+ waitForReady();
+
+ synchronized (mLock) {
+ final VolumeInfo vol = findVolumeById(volId);
+ final VolumeMetadata meta = findOrCreateMetadataLocked(vol);
+ meta.nickname = nickname;
+ refreshMetadataLocked();
+ writeMetadataLocked();
+ mCallbacks.notifyVolumeMetadataChanged(vol.clone());
+ }
+ }
+
+ @Override
+ public void setVolumeUserFlags(String volId, int flags, int mask) {
+ enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+ waitForReady();
+
+ synchronized (mLock) {
+ final VolumeInfo vol = findVolumeById(volId);
+ final VolumeMetadata meta = findOrCreateMetadataLocked(vol);
+ meta.userFlags = (meta.userFlags & ~mask) | (flags & mask);
+ refreshMetadataLocked();
+ writeMetadataLocked();
+ mCallbacks.notifyVolumeMetadataChanged(vol.clone());
+ }
+ }
+
+ @Override
public int[] getStorageUsers(String path) {
enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
waitForReady();
@@ -1909,7 +2109,12 @@
}
@Override
- public VolumeInfo[] getVolumes() {
+ public VolumeInfo[] getVolumes(int flags) {
+ if ((flags & StorageManager.FLAG_ALL_METADATA) != 0) {
+ // TODO: implement support for returning all metadata
+ throw new UnsupportedOperationException();
+ }
+
synchronized (mLock) {
final VolumeInfo[] res = new VolumeInfo[mVolumes.size()];
for (int i = 0; i < mVolumes.size(); i++) {
@@ -2422,6 +2627,7 @@
private static class Callbacks extends Handler {
private static final int MSG_STORAGE_STATE_CHANGED = 1;
private static final int MSG_VOLUME_STATE_CHANGED = 2;
+ private static final int MSG_VOLUME_METADATA_CHANGED = 3;
private final RemoteCallbackList<IMountServiceListener>
mCallbacks = new RemoteCallbackList<>();
@@ -2465,6 +2671,10 @@
callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
break;
}
+ case MSG_VOLUME_METADATA_CHANGED: {
+ callback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+ break;
+ }
}
}
@@ -2483,6 +2693,12 @@
args.argi3 = newState;
obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
}
+
+ private void notifyVolumeMetadataChanged(VolumeInfo vol) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = vol;
+ obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
+ }
}
@Override
@@ -2539,6 +2755,15 @@
vol.dump(pw);
}
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Metadata:");
+ pw.increaseIndent();
+ for (int i = 0; i < mMetadata.size(); i++) {
+ final VolumeMetadata meta = mMetadata.valueAt(i);
+ meta.dump(pw);
+ }
+ pw.decreaseIndent();
}
pw.println();