Merge "Suppress warnings of MultiDexLegacyAndException" into nyc-dev
diff --git a/api/current.txt b/api/current.txt
index 2eba30f..5d28d2c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8534,7 +8534,6 @@
field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL";
field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT";
field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE";
- field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY";
field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED";
field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED";
field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED";
@@ -29365,11 +29364,27 @@
public class StorageManager {
method public java.lang.String getMountedObbPath(java.lang.String);
+ method public android.os.storage.StorageVolume getPrimaryVolume();
+ method public android.os.storage.StorageVolume[] getVolumeList();
method public boolean isObbMounted(java.lang.String);
method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener);
method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener);
}
+ public class StorageVolume implements android.os.Parcelable {
+ method public android.content.Intent createAccessIntent(java.lang.String);
+ method public int describeContents();
+ method public java.lang.String getDescription(android.content.Context);
+ method public java.lang.String getState();
+ method public java.lang.String getUuid();
+ method public boolean isEmulated();
+ method public boolean isPrimary();
+ method public boolean isRemovable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.os.storage.StorageVolume> CREATOR;
+ field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME";
+ }
+
}
package android.preference {
diff --git a/api/system-current.txt b/api/system-current.txt
index 433090e..a2943ff 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8840,7 +8840,6 @@
field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL";
field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT";
field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE";
- field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY";
field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED";
field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED";
field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED";
@@ -24282,19 +24281,26 @@
package android.media.soundtrigger {
public final class SoundTriggerDetector {
- method public boolean startRecognition();
+ method public boolean startRecognition(int);
method public boolean stopRecognition();
+ field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
+ field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
}
- public abstract class SoundTriggerDetector.Callback {
+ public static abstract class SoundTriggerDetector.Callback {
ctor public SoundTriggerDetector.Callback();
method public abstract void onAvailabilityChanged(int);
- method public abstract void onDetected();
+ method public abstract void onDetected(android.media.soundtrigger.SoundTriggerDetector.EventPayload);
method public abstract void onError();
method public abstract void onRecognitionPaused();
method public abstract void onRecognitionResumed();
}
+ public static class SoundTriggerDetector.EventPayload {
+ method public android.media.AudioFormat getCaptureAudioFormat();
+ method public byte[] getTriggerAudio();
+ }
+
public final class SoundTriggerManager {
method public android.media.soundtrigger.SoundTriggerDetector createSoundTriggerDetector(java.util.UUID, android.media.soundtrigger.SoundTriggerDetector.Callback, android.os.Handler);
method public void deleteModel(java.util.UUID);
@@ -31712,11 +31718,27 @@
public class StorageManager {
method public java.lang.String getMountedObbPath(java.lang.String);
+ method public android.os.storage.StorageVolume getPrimaryVolume();
+ method public android.os.storage.StorageVolume[] getVolumeList();
method public boolean isObbMounted(java.lang.String);
method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener);
method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener);
}
+ public class StorageVolume implements android.os.Parcelable {
+ method public android.content.Intent createAccessIntent(java.lang.String);
+ method public int describeContents();
+ method public java.lang.String getDescription(android.content.Context);
+ method public java.lang.String getState();
+ method public java.lang.String getUuid();
+ method public boolean isEmulated();
+ method public boolean isPrimary();
+ method public boolean isRemovable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.os.storage.StorageVolume> CREATOR;
+ field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME";
+ }
+
}
package android.preference {
diff --git a/api/test-current.txt b/api/test-current.txt
index 2fff729..d42c18c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -8539,7 +8539,6 @@
field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL";
field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT";
field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE";
- field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY";
field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED";
field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED";
field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED";
@@ -29375,11 +29374,27 @@
public class StorageManager {
method public java.lang.String getMountedObbPath(java.lang.String);
+ method public android.os.storage.StorageVolume getPrimaryVolume();
+ method public android.os.storage.StorageVolume[] getVolumeList();
method public boolean isObbMounted(java.lang.String);
method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener);
method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener);
}
+ public class StorageVolume implements android.os.Parcelable {
+ method public android.content.Intent createAccessIntent(java.lang.String);
+ method public int describeContents();
+ method public java.lang.String getDescription(android.content.Context);
+ method public java.lang.String getState();
+ method public java.lang.String getUuid();
+ method public boolean isEmulated();
+ method public boolean isPrimary();
+ method public boolean isRemovable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.os.storage.StorageVolume> CREATOR;
+ field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME";
+ }
+
}
package android.preference {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index ea58e29..e3adbda 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -49,6 +49,7 @@
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -3988,8 +3989,12 @@
a.recycle();
if (colorPrimary != 0) {
ActivityManager.TaskDescription td = new ActivityManager.TaskDescription();
- td.setPrimaryColor(colorPrimary);
- td.setBackgroundColor(colorBg);
+ if (Color.alpha(colorPrimary) == 0xFF) {
+ td.setPrimaryColor(colorPrimary);
+ }
+ if (Color.alpha(colorBg) == 0xFF) {
+ td.setBackgroundColor(colorBg);
+ }
setTaskDescription(td);
}
}
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index ab7d708..91eabcc 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.XmlRes;
+import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
@@ -1759,7 +1760,7 @@
return candidates;
}
- private static boolean isPackageCandidateVolume(
+ private boolean isPackageCandidateVolume(
ContextImpl context, ApplicationInfo app, VolumeInfo vol) {
final boolean forceAllowOnExternal = Settings.Global.getInt(
context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
@@ -1789,6 +1790,15 @@
return app.isInternal();
}
+ // Some apps can't be moved. (e.g. device admins)
+ try {
+ if (mPM.isPackageDeviceAdminOnAnyUser(app.packageName)) {
+ return false;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
// Otherwise we can move to any private volume
return (vol.getType() == VolumeInfo.TYPE_PRIVATE);
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5b8e09c..7e7c5ec 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -1033,9 +1033,18 @@
* @hide
*/
public boolean packageHasActiveAdmins(String packageName) {
+ return packageHasActiveAdmins(packageName, myUserId());
+ }
+
+ /**
+ * Used by package administration code to determine if a package can be stopped
+ * or uninstalled.
+ * @hide
+ */
+ public boolean packageHasActiveAdmins(String packageName, int userId) {
if (mService != null) {
try {
- return mService.packageHasActiveAdmins(packageName, myUserId());
+ return mService.packageHasActiveAdmins(packageName, userId);
} catch (RemoteException e) {
Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e);
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b476a25..8f2b9c8 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -3187,38 +3187,6 @@
ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE";
/**
- * Activity Action: Give access to a standard storage directory after obtaining the user's
- * approval.
- * <p>
- * When invoked, the system will ask the user to grant access to the requested directory (and
- * its descendants).
- * <p>
- * To gain access to descendant (child, grandchild, etc) documents, use
- * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)} and
- * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI.
- * <p>
- * Input: full path to a standard directory, in the form of
- * {@code STORAGE_ROOT + STANDARD_DIRECTORY}, where {@code STORAGE_ROOT} is the physical path of
- * a storage container, and {@code STANDARD_DIRECTORY} is one of
- * {@link Environment#DIRECTORY_MUSIC}, {@link Environment#DIRECTORY_PODCASTS},
- * {@link Environment#DIRECTORY_RINGTONES}, {@link Environment#DIRECTORY_ALARMS},
- * {@link Environment#DIRECTORY_NOTIFICATIONS}, {@link Environment#DIRECTORY_PICTURES},
- * {@link Environment#DIRECTORY_MOVIES}, {@link Environment#DIRECTORY_DOWNLOADS},
- * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS}
- * <p>
- * For example, to open the "Pictures" folder in the default external storage, the intent's data
- * would be: {@code Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
- * Environment.DIRECTORY_PICTURES))}.
- * <p>
- * Output: The URI representing the requested directory tree.
- *
- * @see DocumentsContract
- */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String
- ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY";
-
- /**
* Broadcast Action: List of dynamic sensor is changed due to new sensor being connected or
* exisiting sensor being disconnected.
*
@@ -8952,7 +8920,6 @@
case ACTION_MEDIA_SCANNER_SCAN_FILE:
case ACTION_PACKAGE_NEEDS_VERIFICATION:
case ACTION_PACKAGE_VERIFIED:
- case ACTION_OPEN_EXTERNAL_DIRECTORY: // TODO: temporary until bug 26742218 is fixed
// Ignore legacy actions
break;
default:
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index ccb5f82..d6b674c 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -537,4 +537,6 @@
boolean setRequiredForSystemUser(String packageName, boolean systemUserApp);
String getServicesSystemSharedLibraryPackageName();
+
+ boolean isPackageDeviceAdminOnAnyUser(String packageName);
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index c9ee4f3..0967608 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1233,6 +1233,14 @@
public static final int MOVE_FAILED_OPERATION_PENDING = -7;
/**
+ * Error code that is passed to the {@link IPackageMoveObserver} if the
+ * specified package cannot be moved since it contains a device admin.
+ *
+ * @hide
+ */
+ public static final int MOVE_FAILED_DEVICE_ADMIN = -8;
+
+ /**
* Flag parameter for {@link #movePackage} to indicate that
* the package should be moved to internal storage if its
* been installed on external media.
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 70f9cc5..1085b1e 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -339,7 +339,7 @@
* <p>
* Writing to this path requires the
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission,
- * and starting in read access requires the
+ * and starting in {@link android.os.Build.VERSION_CODES#KITKAT}, read access requires the
* {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission,
* which is automatically granted if you hold the write permission.
* <p>
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index e7dfbd7..97ee90d 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -865,7 +865,12 @@
}
}
- /** {@hide} */
+ /**
+ * Gets the list of shared/external storage volumes available to the current user.
+ *
+ * <p>It always contains the primary storage volume, plus any additional external volume(s)
+ * available in the device, such as SD cards or attached USB drives.
+ */
public @NonNull StorageVolume[] getVolumeList() {
return getVolumeList(mContext.getUserId(), 0);
}
@@ -914,7 +919,9 @@
return paths;
}
- /** {@hide} */
+ /**
+ * Gets the primary shared/external storage volume available to the current user.
+ */
public @NonNull StorageVolume getPrimaryVolume() {
return getPrimaryVolume(getVolumeList());
}
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 1408202..d860c7d 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -16,11 +16,17 @@
package android.os.storage;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
+import android.content.Intent;
import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
+import android.provider.DocumentsContract;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
@@ -29,14 +35,47 @@
import java.io.File;
/**
- * Information about a storage volume that may be mounted. This is a legacy
- * specialization of {@link VolumeInfo} which describes the volume for a
- * specific user.
- * <p>
- * This class may be deprecated in the future.
+ * Information about a shared/external storage volume for a specific user.
*
- * @hide
+ * <p>
+ * A device always has one (and one only) primary storage volume, but it could have extra volumes,
+ * like SD cards and USB drives. This object represents the logical view of a storage
+ * volume for a specific user: different users might have different views for the same physical
+ * volume (for example, if the volume is a built-in emulated storage).
+ *
+ * <p>
+ * The storage volume is not necessarily mounted, applications should use {@link #getState()} to
+ * verify its state.
+ *
+ * <p>
+ * Applications willing to read or write to this storage volume needs to get a permission from the
+ * user first, which can be achieved in the following ways:
+ *
+ * <ul>
+ * <li>To get access to standard directories (like the {@link Environment#DIRECTORY_PICTURES}), they
+ * can use the {@link #createAccessIntent(String)}. This is the recommend way, since it provides a
+ * simpler API and narrows the access to the given directory (and its descendants).
+ * <li>To get access to any directory (and its descendants), they can use the Storage Acess
+ * Framework APIs (such as {@link Intent#ACTION_OPEN_DOCUMENT} and
+ * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, although these APIs do not guarantee the user will
+ * select this specific volume.
+ * <li>To get read and write access to the primary storage volume, applications can declare the
+ * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and
+ * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions respectively, with the
+ * latter including the former. This approach is discouraged, since users may be hesitant to grant
+ * broad access to all files contained on a storage device.
+ * </ul>
+ *
+ * <p>It can be obtained through {@link StorageManager#getVolumeList()} and
+ * {@link StorageManager#getPrimaryVolume()} and also as an extra in some broadcasts
+ * (see {@link #EXTRA_STORAGE_VOLUME}).
+ *
+ * <p>
+ * See {@link Environment#getExternalStorageDirectory()} for more info about shared/external
+ * storage semantics.
*/
+// NOTE: This is a legacy specialization of VolumeInfo which describes the volume for a specific
+// user, but is now part of the public API.
public class StorageVolume implements Parcelable {
private final String mId;
@@ -53,14 +92,36 @@
private final String mFsUuid;
private final String mState;
- // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING,
- // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED,
- // ACTION_MEDIA_BAD_REMOVAL, ACTION_MEDIA_UNMOUNTABLE and ACTION_MEDIA_EJECT broadcasts.
- public static final String EXTRA_STORAGE_VOLUME = "storage_volume";
+ /**
+ * Name of the {@link Parcelable} extra in the {@link Intent#ACTION_MEDIA_REMOVED},
+ * {@link Intent#ACTION_MEDIA_UNMOUNTED}, {@link Intent#ACTION_MEDIA_CHECKING},
+ * {@link Intent#ACTION_MEDIA_NOFS}, {@link Intent#ACTION_MEDIA_MOUNTED},
+ * {@link Intent#ACTION_MEDIA_SHARED}, {@link Intent#ACTION_MEDIA_BAD_REMOVAL},
+ * {@link Intent#ACTION_MEDIA_UNMOUNTABLE}, and {@link Intent#ACTION_MEDIA_EJECT} broadcast that
+ * contains a {@link StorageVolume}.
+ */
+ // Also sent on ACTION_MEDIA_UNSHARED, which is @hide
+ public static final String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME";
+ /**
+ * Name of the String extra used by {@link #createAccessIntent(String) createAccessIntent}.
+ *
+ * @hide
+ */
+ public static final String EXTRA_DIRECTORY_NAME = "android.os.storage.extra.DIRECTORY_NAME";
+
+ /**
+ * Name of the intent used by {@link #createAccessIntent(String) createAccessIntent}.
+ */
+ private static final String ACTION_OPEN_EXTERNAL_DIRECTORY =
+ "android.os.storage.action.OPEN_EXTERNAL_DIRECTORY";
+
+ /** {@hide} */
public static final int STORAGE_ID_INVALID = 0x00000000;
+ /** {@hide} */
public static final int STORAGE_ID_PRIMARY = 0x00010001;
+ /** {@hide} */
public StorageVolume(String id, int storageId, File path, String description, boolean primary,
boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage,
long maxFileSize, UserHandle owner, String fsUuid, String state) {
@@ -95,6 +156,7 @@
mState = in.readString();
}
+ /** {@hide} */
public String getId() {
return mId;
}
@@ -103,17 +165,19 @@
* Returns the mount path for the volume.
*
* @return the mount path
+ * @hide
*/
public String getPath() {
return mPath.toString();
}
+ /** {@hide} */
public File getPathFile() {
return mPath;
}
/**
- * Returns a user visible description of the volume.
+ * Returns a user-visible description of the volume.
*
* @return the volume description
*/
@@ -121,6 +185,10 @@
return mDescription;
}
+ /**
+ * Returns true if the volume is the primary shared/external storage, which is the volume
+ * backed by {@link Environment#getExternalStorageDirectory()}.
+ */
public boolean isPrimary() {
return mPrimary;
}
@@ -148,6 +216,7 @@
* this is also used for the storage_id column in the media provider.
*
* @return MTP storage ID
+ * @hide
*/
public int getStorageId() {
return mStorageId;
@@ -164,6 +233,7 @@
* too close to full.
*
* @return MTP reserve space
+ * @hide
*/
public int getMtpReserveSpace() {
return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES);
@@ -173,6 +243,7 @@
* Returns true if this volume can be shared via USB mass storage.
*
* @return whether mass storage is allowed
+ * @hide
*/
public boolean allowMassStorage() {
return mAllowMassStorage;
@@ -182,22 +253,28 @@
* Returns maximum file size for the volume, or zero if it is unbounded.
*
* @return maximum file size
+ * @hide
*/
public long getMaxFileSize() {
return mMaxFileSize;
}
+ /** {@hide} */
public UserHandle getOwner() {
return mOwner;
}
- public String getUuid() {
+ /**
+ * Gets the volume UUID, if any.
+ */
+ public @Nullable String getUuid() {
return mFsUuid;
}
/**
* Parse and return volume UUID as FAT volume ID, or return -1 if unable to
* parse or UUID is unknown.
+ * @hide
*/
public int getFatVolumeId() {
if (mFsUuid == null || mFsUuid.length() != 9) {
@@ -210,14 +287,57 @@
}
}
+ /** {@hide} */
public String getUserLabel() {
return mDescription;
}
+ /**
+ * Returns the current state of the volume.
+ *
+ * @return one of {@link Environment#MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED},
+ * {@link Environment#MEDIA_UNMOUNTED}, {@link Environment#MEDIA_CHECKING},
+ * {@link Environment#MEDIA_NOFS}, {@link Environment#MEDIA_MOUNTED},
+ * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, {@link Environment#MEDIA_SHARED},
+ * {@link Environment#MEDIA_BAD_REMOVAL}, or {@link Environment#MEDIA_UNMOUNTABLE}.
+ */
public String getState() {
return mState;
}
+ /**
+ * Builds an intent to give access to a standard storage directory after obtaining the user's
+ * approval.
+ * <p>
+ * When invoked, the system will ask the user to grant access to the requested directory (and
+ * its descendants). The result of the request will be returned to the activity through the
+ * {@code onActivityResult} method.
+ * <p>
+ * To gain access to descendants (child, grandchild, etc) documents, use
+ * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or
+ * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI.
+ *
+ * <b>If your application only needs to store internal data, consider using
+ * {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs},
+ * {@link Context#getExternalCacheDirs()}, or
+ * {@link Context#getExternalMediaDirs()}, which require no permissions to read or write.
+ *
+ * @param directoryName must be one of
+ * {@link Environment#DIRECTORY_MUSIC}, {@link Environment#DIRECTORY_PODCASTS},
+ * {@link Environment#DIRECTORY_RINGTONES}, {@link Environment#DIRECTORY_ALARMS},
+ * {@link Environment#DIRECTORY_NOTIFICATIONS}, {@link Environment#DIRECTORY_PICTURES},
+ * {@link Environment#DIRECTORY_MOVIES}, {@link Environment#DIRECTORY_DOWNLOADS},
+ * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS}
+ *
+ * @see DocumentsContract
+ */
+ public Intent createAccessIntent(@NonNull String directoryName) {
+ final Intent intent = new Intent(ACTION_OPEN_EXTERNAL_DIRECTORY);
+ intent.putExtra(EXTRA_STORAGE_VOLUME, this);
+ intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName);
+ return intent;
+ }
+
@Override
public boolean equals(Object obj) {
if (obj instanceof StorageVolume && mPath != null) {
@@ -234,11 +354,23 @@
@Override
public String toString() {
+ final StringBuilder buffer = new StringBuilder("StorageVolume: ").append(mDescription);
+ if (mFsUuid != null) {
+ buffer.append(" (").append(mFsUuid).append(")");
+ }
+ return buffer.toString();
+ }
+
+ /** {@hide} */
+ // TODO(b/26742218): find out where toString() is called internally and replace these calls by
+ // dump().
+ public String dump() {
final CharArrayWriter writer = new CharArrayWriter();
dump(new IndentingPrintWriter(writer, " ", 80));
return writer.toString();
}
+ /** {@hide} */
public void dump(IndentingPrintWriter pw) {
pw.println("StorageVolume:");
pw.increaseIndent();
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index b7f071d..3700098 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -39,6 +39,7 @@
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.RemoteException;
+import android.os.storage.StorageVolume;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
@@ -62,7 +63,8 @@
* All client apps must hold a valid URI permission grant to access documents,
* typically issued when a user makes a selection through
* {@link Intent#ACTION_OPEN_DOCUMENT}, {@link Intent#ACTION_CREATE_DOCUMENT},
- * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, or {@link Intent#ACTION_OPEN_EXTERNAL_DIRECTORY}.
+ * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, or
+ * {@link StorageVolume#createAccessIntent(String) StorageVolume.createAccessIntent}.
*
* @see DocumentsProvider
*/
diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
index 982ed68..81cad79 100644
--- a/core/java/android/security/net/config/NetworkSecurityTrustManager.java
+++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
@@ -40,6 +40,9 @@
// TODO: Replace this with a general X509TrustManager and use duck-typing.
private final TrustManagerImpl mDelegate;
private final NetworkSecurityConfig mNetworkSecurityConfig;
+ private final Object mIssuersLock = new Object();
+
+ private X509Certificate[] mIssuers;
public NetworkSecurityTrustManager(NetworkSecurityConfig config) {
if (config == null) {
@@ -139,6 +142,19 @@
@Override
public X509Certificate[] getAcceptedIssuers() {
- return mDelegate.getAcceptedIssuers();
+ // TrustManagerImpl only looks at the provided KeyStore and not the TrustedCertificateStore
+ // for getAcceptedIssuers, so implement it here instead of delegating.
+ synchronized (mIssuersLock) {
+ if (mIssuers == null) {
+ Set<TrustAnchor> anchors = mNetworkSecurityConfig.getTrustAnchors();
+ X509Certificate[] issuers = new X509Certificate[anchors.size()];
+ int i = 0;
+ for (TrustAnchor anchor : anchors) {
+ issuers[i++] = anchor.certificate;
+ }
+ mIssuers = issuers;
+ }
+ return mIssuers.clone();
+ }
}
}
diff --git a/core/java/com/android/internal/app/ISoundTriggerService.aidl b/core/java/com/android/internal/app/ISoundTriggerService.aidl
index 9de4a6c..f4c18c3 100644
--- a/core/java/com/android/internal/app/ISoundTriggerService.aidl
+++ b/core/java/com/android/internal/app/ISoundTriggerService.aidl
@@ -33,10 +33,11 @@
void deleteSoundModel(in ParcelUuid soundModelId);
- void startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
+ int startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback,
+ in SoundTrigger.RecognitionConfig config);
/**
* Stops recognition.
*/
- void stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
+ int stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
}
diff --git a/core/res/res/layout/notification_material_action.xml b/core/res/res/layout/notification_material_action.xml
index 398f52d..548ee05 100644
--- a/core/res/res/layout/notification_material_action.xml
+++ b/core/res/res/layout/notification_material_action.xml
@@ -21,6 +21,7 @@
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="center"
+ android:gravity="start|center_vertical"
android:layout_marginStart="4dp"
android:textColor="@color/notification_default_color"
android:singleLine="true"
diff --git a/core/res/res/layout/notification_material_action_tombstone.xml b/core/res/res/layout/notification_material_action_tombstone.xml
index 976448b..1f59ea0 100644
--- a/core/res/res/layout/notification_material_action_tombstone.xml
+++ b/core/res/res/layout/notification_material_action_tombstone.xml
@@ -18,16 +18,15 @@
<Button xmlns:android="http://schemas.android.com/apk/res/android"
style="@android:style/Widget.Material.Light.Button.Borderless.Small"
android:id="@+id/action0"
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="48dp"
- android:layout_weight="1"
+ android:layout_marginStart="4dp"
+ android:layout_gravity="center"
android:gravity="start|center_vertical"
- android:drawablePadding="8dp"
- android:paddingStart="8dp"
android:textColor="#555555"
- android:textSize="@dimen/notification_text_size"
android:singleLine="true"
android:ellipsize="end"
android:alpha="0.5"
android:enabled="false"
+ android:background="@drawable/notification_material_action_background"
/>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index be8577a..4480944 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5030,7 +5030,7 @@
<attr name="firstDayOfWeek" format="integer" />
<!-- The minimal date shown by this calendar view in mm/dd/yyyy format. -->
<attr name="minDate" />
- <!-- The minimal date shown by this calendar view in mm/dd/yyyy format. -->
+ <!-- The maximal date shown by this calendar view in mm/dd/yyyy format. -->
<attr name="maxDate" />
<!-- The text appearance for the month and year in the calendar header. -->
<attr name="monthTextAppearance" format="reference" />
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index f09f654..60444e0 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -277,6 +277,30 @@
public static final int HOTWORD = 1999;
}
+ // TODO make AudioSource static (API change) and move this method inside the AudioSource class
+ /**
+ * @hide
+ * @param source An audio source to test
+ * @return true if the source is only visible to system components
+ */
+ public static boolean isSystemOnlyAudioSource(int source) {
+ switch(source) {
+ case AudioSource.DEFAULT:
+ case AudioSource.MIC:
+ case AudioSource.VOICE_UPLINK:
+ case AudioSource.VOICE_DOWNLINK:
+ case AudioSource.VOICE_CALL:
+ case AudioSource.CAMCORDER:
+ case AudioSource.VOICE_RECOGNITION:
+ case AudioSource.VOICE_COMMUNICATION:
+ //case REMOTE_SUBMIX: considered "system" as it requires system permissions
+ case AudioSource.UNPROCESSED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
/**
* Defines the video source. These constants are used with
* {@link MediaRecorder#setVideoSource(int)}.
diff --git a/media/java/android/media/soundtrigger/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
index 707db06..8f022db 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerDetector.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
@@ -16,12 +16,17 @@
package android.media.soundtrigger;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.AudioFormat;
import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Slog;
@@ -29,6 +34,8 @@
import com.android.internal.app.ISoundTriggerService;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
/**
@@ -45,6 +52,12 @@
private static final boolean DBG = false;
private static final String TAG = "SoundTriggerDetector";
+ private static final int MSG_AVAILABILITY_CHANGED = 1;
+ private static final int MSG_SOUND_TRIGGER_DETECTED = 2;
+ private static final int MSG_DETECTION_ERROR = 3;
+ private static final int MSG_DETECTION_PAUSE = 4;
+ private static final int MSG_DETECTION_RESUME = 5;
+
private final Object mLock = new Object();
private final ISoundTriggerService mSoundTriggerService;
@@ -53,7 +66,121 @@
private final Handler mHandler;
private final RecognitionCallback mRecognitionCallback;
- public abstract class Callback {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {
+ RECOGNITION_FLAG_NONE,
+ RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
+ RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
+ })
+ public @interface RecognitionFlags {}
+
+ /**
+ * Empty flag for {@link #startRecognition(int)}.
+ *
+ * @hide
+ */
+ public static final int RECOGNITION_FLAG_NONE = 0;
+
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the trigger audio for hotword needs to be captured.
+ */
+ public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
+
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the recognition should keep going on even after the
+ * model triggers.
+ * If this flag is specified, it's possible to get multiple
+ * triggers after a call to {@link #startRecognition(int)}, if the model
+ * triggers multiple times.
+ * When this isn't specified, the default behavior is to stop recognition once the
+ * trigger happenss, till the caller starts recognition again.
+ */
+ public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2;
+
+ /**
+ * Additional payload for {@link Callback#onDetected}.
+ */
+ public static class EventPayload {
+ private final boolean mTriggerAvailable;
+
+ // Indicates if {@code captureSession} can be used to continue capturing more audio
+ // from the DSP hardware.
+ private final boolean mCaptureAvailable;
+ // The session to use when attempting to capture more audio from the DSP hardware.
+ private final int mCaptureSession;
+ private final AudioFormat mAudioFormat;
+ // Raw data associated with the event.
+ // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
+ private final byte[] mData;
+
+ private EventPayload(boolean triggerAvailable, boolean captureAvailable,
+ AudioFormat audioFormat, int captureSession, byte[] data) {
+ mTriggerAvailable = triggerAvailable;
+ mCaptureAvailable = captureAvailable;
+ mCaptureSession = captureSession;
+ mAudioFormat = audioFormat;
+ mData = data;
+ }
+
+ /**
+ * Gets the format of the audio obtained using {@link #getTriggerAudio()}.
+ * May be null if there's no audio present.
+ */
+ @Nullable
+ public AudioFormat getCaptureAudioFormat() {
+ return mAudioFormat;
+ }
+
+ /**
+ * Gets the raw audio that triggered the keyphrase.
+ * This may be null if the trigger audio isn't available.
+ * If non-null, the format of the audio can be obtained by calling
+ * {@link #getCaptureAudioFormat()}.
+ *
+ * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
+ */
+ @Nullable
+ public byte[] getTriggerAudio() {
+ if (mTriggerAvailable) {
+ return mData;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the session ID to start a capture from the DSP.
+ * This may be null if streaming capture isn't possible.
+ * If non-null, the format of the audio that can be captured can be
+ * obtained using {@link #getCaptureAudioFormat()}.
+ *
+ * TODO: Candidate for Public API when the API to start capture with a session ID
+ * is made public.
+ *
+ * TODO: Add this to {@link #getCaptureAudioFormat()}:
+ * "Gets the format of the audio obtained using {@link #getTriggerAudio()}
+ * or {@link #getCaptureSession()}. May be null if no audio can be obtained
+ * for either the trigger or a streaming session."
+ *
+ * TODO: Should this return a known invalid value instead?
+ *
+ * @hide
+ */
+ @Nullable
+ public Integer getCaptureSession() {
+ if (mCaptureAvailable) {
+ return mCaptureSession;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public static abstract class Callback {
/**
* Called when the availability of the sound model changes.
*/
@@ -63,7 +190,7 @@
* Called when the sound model has triggered (such as when it matched a
* given sound pattern).
*/
- public abstract void onDetected();
+ public abstract void onDetected(@NonNull EventPayload eventPayload);
/**
* Called when the detection fails due to an error.
@@ -95,9 +222,9 @@
mSoundModelId = soundModelId;
mCallback = callback;
if (handler == null) {
- mHandler = new Handler();
+ mHandler = new MyHandler();
} else {
- mHandler = handler;
+ mHandler = new MyHandler(handler.getLooper());
}
mRecognitionCallback = new RecognitionCallback();
}
@@ -107,13 +234,19 @@
* {@link Callback}.
* @return Indicates whether the call succeeded or not.
*/
- public boolean startRecognition() {
+ public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
if (DBG) {
Slog.d(TAG, "startRecognition()");
}
+ boolean captureTriggerAudio =
+ (recognitionFlags & RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
+
+ boolean allowMultipleTriggers =
+ (recognitionFlags & RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
try {
mSoundTriggerService.startRecognition(new ParcelUuid(mSoundModelId),
- mRecognitionCallback);
+ mRecognitionCallback, new RecognitionConfig(captureTriggerAudio,
+ allowMultipleTriggers, null, null));
} catch (RemoteException e) {
return false;
}
@@ -144,17 +277,25 @@
/**
* Callback that handles events from the lower sound trigger layer.
+ *
+ * Note that these callbacks will be called synchronously from the SoundTriggerService
+ * layer and thus should do minimal work (such as sending a message on a handler to do
+ * the real work).
* @hide
*/
- private static class RecognitionCallback extends
- IRecognitionStatusCallback.Stub {
+ private class RecognitionCallback extends IRecognitionStatusCallback.Stub {
/**
* @hide
*/
@Override
public void onDetected(SoundTrigger.RecognitionEvent event) {
- Slog.e(TAG, "onDetected()" + event);
+ Slog.d(TAG, "onDetected()" + event);
+ Message.obtain(mHandler,
+ MSG_SOUND_TRIGGER_DETECTED,
+ new EventPayload(event.triggerInData, event.captureAvailable,
+ event.captureFormat, event.captureSession, event.data))
+ .sendToTarget();
}
/**
@@ -162,7 +303,8 @@
*/
@Override
public void onError(int status) {
- Slog.e(TAG, "onError()" + status);
+ Slog.d(TAG, "onError()" + status);
+ mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
}
/**
@@ -170,7 +312,8 @@
*/
@Override
public void onRecognitionPaused() {
- Slog.e(TAG, "onRecognitionPaused()");
+ Slog.d(TAG, "onRecognitionPaused()");
+ mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
}
/**
@@ -178,7 +321,44 @@
*/
@Override
public void onRecognitionResumed() {
- Slog.e(TAG, "onRecognitionResumed()");
+ Slog.d(TAG, "onRecognitionResumed()");
+ mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
+ }
+ }
+
+ private class MyHandler extends Handler {
+
+ MyHandler() {
+ super();
+ }
+
+ MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Received message: " + msg.what + " for NULL callback.");
+ return;
+ }
+ switch (msg.what) {
+ case MSG_SOUND_TRIGGER_DETECTED:
+ mCallback.onDetected((EventPayload) msg.obj);
+ break;
+ case MSG_DETECTION_ERROR:
+ mCallback.onError();
+ break;
+ case MSG_DETECTION_PAUSE:
+ mCallback.onRecognitionPaused();
+ break;
+ case MSG_DETECTION_RESUME:
+ mCallback.onRecognitionResumed();
+ break;
+ default:
+ super.handleMessage(msg);
+
+ }
}
}
}
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 9ac929b..58e7709 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -94,9 +94,8 @@
android:name=".OpenExternalDirectoryActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
- <action android:name="android.intent.action.OPEN_EXTERNAL_DIRECTORY" />
+ <action android:name="android.os.storage.action.OPEN_EXTERNAL_DIRECTORY" />
<category android:name="android.intent.category.DEFAULT" />
- <data android:scheme="file" />
</intent-filter>
</activity>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index d0bb7e0..29bb5e4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -134,9 +134,13 @@
}
if (state.action == ACTION_PICK_COPY_DESTINATION) {
+ // Indicates that a copy operation (or move) includes a directory.
+ // Why? Directory creation isn't supported by some roots (like Downloads).
+ // This allows us to restrict available roots to just those with support.
state.directoryCopy = intent.getBooleanExtra(
Shared.EXTRA_DIRECTORY_COPY, false);
- state.transferMode = intent.getIntExtra(FileOperationService.EXTRA_OPERATION,
+ state.copyOperationSubType = intent.getIntExtra(
+ FileOperationService.EXTRA_OPERATION,
FileOperationService.OPERATION_COPY);
}
}
@@ -156,6 +160,9 @@
if (external && mState.action == ACTION_GET_CONTENT) {
showDrawer = true;
}
+ if (mState.action == ACTION_PICK_COPY_DESTINATION) {
+ showDrawer = true;
+ }
if (showDrawer) {
mNavigator.revealRootsDrawer(true);
@@ -307,7 +314,7 @@
mState.action == ACTION_PICK_COPY_DESTINATION) {
final PickFragment pick = PickFragment.get(fm);
if (pick != null) {
- pick.setPickTarget(mState.action, mState.transferMode, cwd);
+ pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd);
}
}
}
@@ -420,7 +427,7 @@
// Picking a copy destination is only used internally by us, so we
// don't need to extend permissions to the caller.
intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
- intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.transferMode);
+ intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
} else {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index d601550..025faea 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -17,6 +17,8 @@
package com.android.documentsui;
import static android.os.Environment.isStandardDirectory;
+import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
+import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
import static com.android.documentsui.Shared.DEBUG;
import android.app.Activity;
@@ -35,9 +37,11 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
import android.provider.DocumentsContract;
import android.text.TextUtils;
@@ -63,16 +67,31 @@
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
- if (intent == null || intent.getData() == null) {
- Log.d(TAG, "missing intent or intent data: " + intent);
+ if (intent == null) {
+ if (DEBUG) Log.d(TAG, "missing intent");
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
+ if (!(storageVolume instanceof StorageVolume)) {
+ if (DEBUG)
+ Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
+ + storageVolume);
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ final String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME);
+ if (directoryName == null) {
+ if (DEBUG) Log.d(TAG, "missing extra " + EXTRA_DIRECTORY_NAME + " on " + intent);
setResult(RESULT_CANCELED);
finish();
return;
}
- final String path = intent.getData().getPath();
final int userId = UserHandle.myUserId();
- if (!showFragment(this, userId, path)) {
+ if (!showFragment(this, userId, (StorageVolume) storageVolume, directoryName)) {
setResult(RESULT_CANCELED);
finish();
return;
@@ -80,20 +99,20 @@
}
/**
- * Validates the given {@code path} and display the appropriate dialog asking the user to grant
- * access to it.
+ * Validates the given path (volume + directory) and display the appropriate dialog asking the
+ * user to grant access to it.
*/
- static boolean showFragment(Activity activity, int userId, String path) {
- Log.d(TAG, "showFragment() for path " + path + " and user " + userId);
- if (path == null) {
- Log.e(TAG, "INTERNAL ERROR: showFragment() with null path");
- return false;
- }
+ private static boolean showFragment(Activity activity, int userId, StorageVolume storageVolume,
+ String directoryName) {
+ if (DEBUG)
+ Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
+ + directoryName + ", and user " + userId);
File file;
try {
- file = new File(new File(path).getCanonicalPath());
+ file = new File(storageVolume.getPathFile(), directoryName).getCanonicalFile();
} catch (IOException e) {
- Log.e(TAG, "Could not get canonical file from " + path);
+ Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
+ + " and directory " + directoryName);
return false;
}
final StorageManager sm =
@@ -104,7 +123,9 @@
// Verify directory is valid.
if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
- Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '" + path + "')");
+ if (DEBUG)
+ Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
+ + file.getAbsolutePath() + "')");
return false;
}
@@ -123,7 +144,7 @@
}
}
if (volumeLabel == null) {
- Log.e(TAG, "Could not get volume for " + path);
+ Log.e(TAG, "Could not get volume for " + file);
return false;
}
@@ -165,13 +186,13 @@
final File userPath = volume.getPathForUser(userId);
final String path = userPath == null ? null : volume.getPathForUser(userId).getPath();
final boolean isVisible = volume.isVisibleForWrite(userId);
- if (DEBUG) {
+ if (DEBUG)
Log.d(TAG, "Volume: " + volume + " userId: " + userId + " root: " + root
+ " volumePath: " + volume.getPath().getPath()
+ " pathForUser: " + path
+ " internalPathForUser: " + volume.getInternalPath()
+ " isVisible: " + isVisible);
- }
+
return volume.isVisibleForWrite(userId) && root.equals(path);
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
index bbf4682..287c904 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -16,6 +16,10 @@
package com.android.documentsui;
+import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
+import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
+import static com.android.internal.util.Preconditions.checkArgument;
+
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -27,6 +31,7 @@
import android.widget.Button;
import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.services.FileOperationService.OpType;
/**
* Display pick confirmation bar, usually for selecting a directory.
@@ -35,7 +40,7 @@
public static final String TAG = "PickFragment";
private int mAction;
- private int mTransferMode;
+ private @OpType int mOperationType;
private DocumentInfo mPickTarget;
private View mContainer;
private Button mPick;
@@ -92,9 +97,10 @@
/**
* @param action Which action defined in State is the picker shown for.
*/
- public void setPickTarget(int action, int transferMode, DocumentInfo pickTarget) {
+ public void setPickTarget(int action, @OpType int operationType, DocumentInfo pickTarget) {
+ checkArgument(operationType == OPERATION_COPY || operationType == OPERATION_MOVE);
mAction = action;
- mTransferMode = transferMode;
+ mOperationType = operationType;
mPickTarget = pickTarget;
if (mContainer != null) {
updateView();
@@ -111,7 +117,8 @@
mCancel.setVisibility(View.GONE);
break;
case State.ACTION_PICK_COPY_DESTINATION:
- mPick.setText(R.string.button_copy);
+ mPick.setText(mOperationType == OPERATION_MOVE
+ ? R.string.button_move : R.string.button_copy);
mCancel.setVisibility(View.VISIBLE);
break;
default:
diff --git a/packages/DocumentsUI/src/com/android/documentsui/QuickViewIntentBuilder.java b/packages/DocumentsUI/src/com/android/documentsui/QuickViewIntentBuilder.java
index c34cec0..a77a9b3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/QuickViewIntentBuilder.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/QuickViewIntentBuilder.java
@@ -33,16 +33,19 @@
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Range;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.model.DocumentInfo;
+import java.util.ArrayList;
import java.util.List;
/**
* Provides support for gather a list of quick-viewable files into a quick view intent.
*/
final class QuickViewIntentBuilder {
+ private static final int MAX_CLIP_ITEMS = 1000;
private final DocumentInfo mDocument;
private final Model mModel;
@@ -50,9 +53,6 @@
private final PackageManager mPkgManager;
private final Resources mResources;
- private ClipData mClipData;
- private int mDocumentLocation;
-
public QuickViewIntentBuilder(
PackageManager pkgManager,
Resources resources,
@@ -80,12 +80,28 @@
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setPackage(trustedPkg);
if (hasRegisteredHandler(intent)) {
- List<String> siblingIds = mModel.getModelIds();
- for (int i = 0; i < siblingIds.size(); i++) {
- onNextItem(i, siblingIds);
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ final int documentLocation = collectViewableUris(uris);
+ final Range<Integer> range = computeSiblingsRange(uris, documentLocation);
+
+ ClipData clipData = null;
+ ClipData.Item item;
+ Uri uri;
+ for (int i = range.getLower(); i <= range.getUpper(); i++) {
+ uri = uris.get(i);
+ item = new ClipData.Item(uri);
+ if (DEBUG) Log.d(TAG, "Including file: " + uri);
+ if (clipData == null) {
+ clipData = new ClipData(
+ "URIs", new String[] { ClipDescription.MIMETYPE_TEXT_URILIST },
+ item);
+ } else {
+ clipData.addItem(item);
+ }
}
- intent.putExtra(Intent.EXTRA_INDEX, mDocumentLocation);
- intent.setClipData(mClipData);
+
+ intent.putExtra(Intent.EXTRA_INDEX, documentLocation);
+ intent.setClipData(clipData);
return intent;
} else {
@@ -96,39 +112,63 @@
return null;
}
+ private int collectViewableUris(ArrayList<Uri> uris) {
+ final List<String> siblingIds = mModel.getModelIds();
+ uris.ensureCapacity(siblingIds.size());
+
+ int documentLocation = 0;
+ Cursor cursor;
+ String mimeType;
+ String id;
+ String authority;
+ Uri uri;
+
+ // Cursor's are not guaranteed to be immutable. Hence, traverse it only once.
+ for (int i = 0; i < siblingIds.size(); i++) {
+ cursor = mModel.getItem(siblingIds.get(i));
+
+ mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+ if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+ continue;
+ }
+
+ id = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
+ authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
+ uri = DocumentsContract.buildDocumentUri(authority, id);
+
+ if (id.equals(mDocument.documentId)) {
+ if (DEBUG) Log.d(TAG, "Found starting point for QV. " + i);
+ documentLocation = i;
+ }
+
+ uris.add(uri);
+ }
+
+ return documentLocation;
+ }
+
+ private static Range<Integer> computeSiblingsRange(List<Uri> uris, int documentLocation) {
+ // Restrict number of siblings to avoid hitting the IPC limit.
+ // TODO: Remove this restriction once ClipData can hold an arbitrary number of
+ // items.
+ int firstSibling;
+ int lastSibling;
+ if (documentLocation < uris.size() / 2) {
+ firstSibling = Math.max(0, documentLocation - MAX_CLIP_ITEMS / 2);
+ lastSibling = Math.min(uris.size() - 1, firstSibling + MAX_CLIP_ITEMS - 1);
+ } else {
+ lastSibling = Math.min(uris.size() - 1, documentLocation + MAX_CLIP_ITEMS / 2);
+ firstSibling = Math.max(0, lastSibling - MAX_CLIP_ITEMS + 1);
+ }
+
+ if (DEBUG) Log.d(TAG, "Copmuted siblings from index: " + firstSibling
+ + " to: " + lastSibling);
+
+ return new Range(firstSibling, lastSibling);
+ }
+
private boolean hasRegisteredHandler(Intent intent) {
// Try to resolve the intent. If a matching app isn't installed, it won't resolve.
return intent.resolveActivity(mPkgManager) != null;
}
-
- private void onNextItem(int index, List<String> siblingIds) {
- final Cursor cursor = mModel.getItem(siblingIds.get(index));
-
- if (cursor == null) {
- return;
- }
-
- String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
- if (Document.MIME_TYPE_DIR.equals(mimeType)) {
- return;
- }
-
- String id = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
- String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
- Uri uri = DocumentsContract.buildDocumentUri(authority, id);
- if (DEBUG) Log.d(TAG, "Including file[" + id + "] @ " + uri);
-
- if (id.equals(mDocument.documentId)) {
- if (DEBUG) Log.d(TAG, "Found starting point for QV. " + index);
- mDocumentLocation = index;
- }
-
- ClipData.Item item = new ClipData.Item(uri);
- if (mClipData == null) {
- mClipData = new ClipData(
- "URIs", new String[]{ClipDescription.MIMETYPE_TEXT_URILIST}, item);
- } else {
- mClipData.addItem(item);
- }
- }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/State.java b/packages/DocumentsUI/src/com/android/documentsui/State.java
index 7dca8a7..81a0635 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/State.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/State.java
@@ -30,6 +30,7 @@
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.services.FileOperationService.OpType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -83,10 +84,18 @@
public boolean forceAdvanced;
public boolean showAdvanced;
public boolean restored;
+
+ // Indicates that a copy operation (or move) includes a directory.
+ // Why? Directory creation isn't supported by some roots (like Downloads).
+ // This allows us to restrict available roots to just those with support.
public boolean directoryCopy;
public boolean openableOnly;
- /** Transfer mode for file copy/move operations. */
- public int transferMode;
+
+ /**
+ * This is basically a sub-type for the copy operation. It can be either COPY or MOVE.
+ * The only legal values are: OPERATION_COPY, OPERATION_MOVE.
+ */
+ public @OpType int copyOperationSubType;
/** Current user navigation stack; empty implies recents. */
public DocumentStack stack = new DocumentStack();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 0ae2a5c..0c851c8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -101,6 +101,7 @@
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
+
import com.google.common.collect.Lists;
import java.lang.annotation.Retention;
@@ -130,6 +131,11 @@
public static final int ANIM_LEAVE = 3;
public static final int ANIM_ENTER = 4;
+ @IntDef(flag = true, value = {
+ REQUEST_COPY_DESTINATION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequestCode {}
public static final int REQUEST_COPY_DESTINATION = 1;
static final boolean DEBUG_ENABLE_DND = true;
@@ -193,11 +199,6 @@
mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
- // Make the RecyclerView unfocusable. This is needed in order for the focus search code in
- // FocusManager to work correctly. Setting android:focusable=false in the layout xml doesn't
- // work, for some reason.
- mRecView.setFocusable(false);
-
// TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
if (DEBUG_ENABLE_DND) {
setupDragAndDropOnDirectoryView(mRecView);
@@ -377,19 +378,24 @@
}
@Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- // There's only one request code right now. Replace this with a switch statement or
- // something more scalable when more codes are added.
- if (requestCode != REQUEST_COPY_DESTINATION) {
- return;
+ public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
+ switch(requestCode) {
+ case REQUEST_COPY_DESTINATION:
+ handleCopyResult(resultCode, data);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown request code: " + requestCode);
}
+ }
+
+ private void handleCopyResult(int resultCode, Intent data) {
if (resultCode == Activity.RESULT_CANCELED || data == null) {
// User pressed the back button or otherwise cancelled the destination pick. Don't
// proceed with the copy.
return;
}
- int operationType = data.getIntExtra(
+ @OpType int operationType = data.getIntExtra(
FileOperationService.EXTRA_OPERATION,
FileOperationService.OPERATION_COPY);
@@ -808,25 +814,43 @@
getActivity(),
DocumentsActivity.class);
+ // Set an appropriate title on the drawer when it is shown in the picker.
+ // Coupled with the fact that we auto-open the drawer for copy/move operations
+ // it should basically be the thing people see first.
+ int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
+ ? R.string.menu_move : R.string.menu_copy;
+ intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
+
new GetDocumentsTask() {
@Override
void onDocumentsReady(List<DocumentInfo> docs) {
+ // TODO: Can this move to Fragment bundle state?
getDisplayState().selectedDocumentsForCopy = docs;
- boolean directoryCopy = false;
- for (DocumentInfo info : docs) {
- if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
- directoryCopy = true;
- break;
- }
- }
- intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, directoryCopy);
+ // Determine if there is a directory in the set of documents
+ // to be copied? Why? Directory creation isn't supported by some roots
+ // (like Downloads). This informs DocumentsActivity (the "picker")
+ // to restrict available roots to just those with support.
+ intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
+
+ // This just identifies the type of request...we'll check it
+ // when we reveive a response.
startActivityForResult(intent, REQUEST_COPY_DESTINATION);
}
+
}.execute(selected);
}
+ private static boolean hasDirectory(List<DocumentInfo> docs) {
+ for (DocumentInfo info : docs) {
+ if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void renameDocuments(Selection selected) {
// Batch renaming not supported
// Rename option is only available in menu when 1 document selected
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
index 93ec842..e90a447 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
@@ -158,7 +158,14 @@
}
if (searchDir != -1) {
+ // Focus search behaves badly if the parent RecyclerView is focused. However, focusable
+ // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after
+ // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable
+ // off while performing the focus search.
+ // TODO: Revisit this when RV focus issues are resolved.
+ mView.setFocusable(false);
View targetView = view.focusSearch(searchDir);
+ mView.setFocusable(true);
// TargetView can be null, for example, if the user pressed <down> at the bottom
// of the list.
if (targetView != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
index 3a025c2..05a3f11 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
@@ -71,11 +71,6 @@
// such case, this needs to be replaced with pairs of parent and child.
public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
- public static final int OPERATION_UNKNOWN = -1;
- public static final int OPERATION_COPY = 1;
- public static final int OPERATION_MOVE = 2;
- public static final int OPERATION_DELETE = 3;
-
@IntDef(flag = true, value = {
OPERATION_UNKNOWN,
OPERATION_COPY,
@@ -84,6 +79,10 @@
})
@Retention(RetentionPolicy.SOURCE)
public @interface OpType {}
+ public static final int OPERATION_UNKNOWN = -1;
+ public static final int OPERATION_COPY = 1;
+ public static final int OPERATION_MOVE = 2;
+ public static final int OPERATION_DELETE = 3;
// TODO: Move it to a shared file when more operations are implemented.
public static final int FAILURE_COPY = 1;
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
index 609dc0c..95515db 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -52,7 +52,7 @@
"Videos",
"Audio",
"Downloads",
- "Home",
+ "Documents",
ROOT_0_ID,
ROOT_1_ID);
}
@@ -64,11 +64,11 @@
bot.assertHasDocuments("file0.log", "file1.png", "file2.csv");
}
- public void testLoadsHomeByDefault() throws Exception {
+ public void testLoadsHomeDirectoryByDefault() throws Exception {
initTestFiles();
device.waitForIdle();
- bot.assertWindowTitle("Home");
+ bot.assertWindowTitle("Documents");
}
public void testRootClickSetsWindowTitle() throws Exception {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 9650651..7012eb2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -626,12 +626,12 @@
<!-- UI debug setting: force all activites to be resizable for multiwindow [CHAR LIMIT=50] -->
<string name="force_resizable_activities">Force activities to be resizable</string>
<!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] -->
- <string name="force_resizable_activities_summary">Makes all activities resizable for multi-window, regardless of manifest values.</string>
+ <string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string>
<!-- UI debug setting: enable freeform window support [CHAR LIMIT=50] -->
<string name="enable_freeform_support">Enable freeform windows</string>
<!-- UI debug setting: enable freeform window support summary [CHAR LIMIT=150] -->
- <string name="enable_freeform_support_summary">Enables support for experimental freeform windows.</string>
+ <string name="enable_freeform_support_summary">Enable support for experimental freeform windows.</string>
<!-- Local (desktop) backup password menu title [CHAR LIMIT=25] -->
<string name="local_backup_password_title">Desktop backup password</string>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 84df0d6..26152cd 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -34,19 +34,31 @@
<LinearLayout
android:id="@+id/expanded_group"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
+ android:layout_height="48dp"
android:gravity="center"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal"
android:layout_alignParentEnd="true"
- android:layout_marginTop="30dp"
- android:layout_marginEnd="16dp">
+ android:layout_marginTop="28dp"
+ android:layout_marginEnd="12dp">
+
+ <com.android.systemui.statusbar.phone.MultiUserSwitch android:id="@+id/multi_user_switch"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentEnd="true"
+ android:background="@drawable/ripple_drawable" >
+ <ImageView android:id="@+id/multi_user_avatar"
+ android:layout_width="@dimen/multi_user_avatar_expanded_size"
+ android:layout_height="@dimen/multi_user_avatar_expanded_size"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"/>
+ </com.android.systemui.statusbar.phone.MultiUserSwitch>
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/settings_button_container"
android:layout_width="48dp"
- android:layout_height="@dimen/status_bar_header_height"
+ android:layout_height="48dp"
android:clipChildren="false"
android:clipToPadding="false">
@@ -68,18 +80,6 @@
</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
- <com.android.systemui.statusbar.phone.MultiUserSwitch android:id="@+id/multi_user_switch"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_alignParentEnd="true"
- android:background="@drawable/ripple_drawable" >
- <ImageView android:id="@+id/multi_user_avatar"
- android:layout_width="@dimen/multi_user_avatar_expanded_size"
- android:layout_height="@dimen/multi_user_avatar_expanded_size"
- android:layout_gravity="center"
- android:scaleType="centerInside"/>
- </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
@@ -104,44 +104,62 @@
android:gravity="center_vertical" />
<LinearLayout
- android:id="@+id/date_time_group"
+ android:id="@+id/date_time_alarm_group"
android:layout_width="wrap_content"
- android:layout_height="25dp"
+ android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
- android:orientation="horizontal">
-
- <include layout="@layout/split_clock_view"
+ android:layout_marginStart="16dp"
+ android:gravity="start"
+ android:orientation="vertical">
+ <LinearLayout
+ android:id="@+id/date_time_group"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginStart="16dp"
+ android:layout_height="19dp"
android:layout_marginTop="4dp"
- android:id="@+id/clock" />
+ android:orientation="horizontal">
- <com.android.systemui.statusbar.policy.DateView
- android:id="@+id/date"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginStart="6dp"
- android:layout_marginTop="4dp"
- android:drawableStart="@drawable/header_dot"
- android:drawablePadding="6dp"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock"
- android:textSize="@dimen/qs_time_collapsed_size"
- android:gravity="top"
- systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" />
+ <include layout="@layout/split_clock_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/clock" />
+
+ <com.android.systemui.statusbar.policy.DateView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="6dp"
+ android:drawableStart="@drawable/header_dot"
+ android:drawablePadding="6dp"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock"
+ android:textSize="@dimen/qs_time_collapsed_size"
+ android:gravity="top"
+ systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" />
+
+ <com.android.systemui.statusbar.AlphaOptimizedButton
+ android:id="@+id/alarm_status_collapsed"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawablePadding="6dp"
+ android:drawableStart="@drawable/ic_access_alarms_small"
+ android:textColor="#64ffffff"
+ android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Date"
+ android:paddingStart="6dp"
+ android:gravity="top"
+ android:background="?android:attr/selectableItemBackground"
+ android:visibility="gone" />
+ </LinearLayout>
<com.android.systemui.statusbar.AlphaOptimizedButton
android:id="@+id/alarm_status"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginTop="4dp"
- android:drawablePadding="6dp"
+ android:layout_height="20dp"
+ android:paddingTop="3dp"
+ android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_access_alarms_small"
android:textColor="#64ffffff"
android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Date"
- android:paddingStart="6dp"
android:gravity="top"
android:background="?android:attr/selectableItemBackground"
android:visibility="gone" />
@@ -152,7 +170,7 @@
android:background="#0000"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="25dp"
+ android:layout_marginTop="28dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_alignParentEnd="true"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 11c13e1..aed5ab2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -162,7 +162,10 @@
<dimen name="qs_tile_margin">16dp</dimen>
<dimen name="qs_quick_tile_size">48dp</dimen>
<dimen name="qs_quick_tile_padding">12dp</dimen>
- <dimen name="qs_date_anim_translation">44.5dp</dimen>
+ <dimen name="qs_date_anim_translation">36dp</dimen>
+ <dimen name="qs_date_alarm_anim_translation">26dp</dimen>
+ <dimen name="qs_date_collapsed_text_size">14sp</dimen>
+ <dimen name="qs_date_text_size">16sp</dimen>
<dimen name="qs_page_indicator_size">12dp</dimen>
<dimen name="qs_tile_icon_size">24dp</dimen>
<dimen name="qs_tile_text_size">12sp</dimen>
@@ -598,9 +601,6 @@
<dimen name="fab_elevation">12dp</dimen>
<dimen name="fab_press_translation_z">9dp</dimen>
- <!-- TODO: Remove this -->
- <dimen name="qs_header_neg_padding">-8dp</dimen>
-
<!-- How high we lift the divider when touching -->
<dimen name="docked_stack_divider_lift_elevation">4dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ec04861..fa5b1a9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1182,7 +1182,7 @@
<string name="overview_disable_fast_toggle_via_button_desc">Disable launch timeout while paging</string>
<!-- Toggle to enable the gesture to enter split-screen by swiping up from the Overview button. [CHAR LIMIT=60]-->
- <string name="overview_nav_bar_gesture">Enable split-screen swipe-up accelerator</string>
+ <string name="overview_nav_bar_gesture">Enable split-screen swipe-up gesture</string>
<!-- Description for the toggle to enable the gesture to enter split-screen by swiping up from the Overview button. [CHAR LIMIT=NONE]-->
<string name="overview_nav_bar_gesture_desc">Enable gesture to enter split-screen by swiping up from the Overview button</string>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
index 11d99ff..3bb141a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
@@ -20,6 +20,7 @@
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
@@ -30,6 +31,7 @@
import android.widget.TextView;
import android.widget.Toast;
import com.android.keyguard.KeyguardStatusView;
+import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QSTile;
@@ -47,7 +49,9 @@
private NextAlarmController mNextAlarmController;
private SettingsButton mSettingsButton;
private View mSettingsContainer;
+
private TextView mAlarmStatus;
+ private TextView mAlarmStatusCollapsed;
private QSPanel mQsPanel;
@@ -56,19 +60,21 @@
private ViewGroup mExpandedGroup;
private ViewGroup mDateTimeGroup;
- private View mEmergencyOnly;
- private TextView mQsDetailHeaderTitle;
+ private ViewGroup mDateTimeAlarmGroup;
+ private TextView mEmergencyOnly;
+
private boolean mListening;
private AlarmManager.AlarmClockInfo mNextAlarm;
private QuickQSPanel mHeaderQsPanel;
private boolean mShowEmergencyCallsOnly;
- private float mDateTimeTranslation;
private MultiUserSwitch mMultiUserSwitch;
private ImageView mMultiUserAvatar;
- private View mQsDetailHeaderBack;
- private final int[] mTmpInt2 = new int[2];
+ private float mDateTimeTranslation;
+ private float mDateTimeAlarmTranslation;
+ private float mExpansionFraction;
+ private float mDateScaleFactor;
public QuickStatusBarHeader(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -78,11 +84,12 @@
protected void onFinishInflate() {
super.onFinishInflate();
- mEmergencyOnly = findViewById(R.id.header_emergency_calls_only);
- mDateTimeTranslation = mContext.getResources().getDimension(
- R.dimen.qs_date_anim_translation);
+ mEmergencyOnly = (TextView) findViewById(R.id.header_emergency_calls_only);
+
+ mDateTimeAlarmGroup = (ViewGroup) findViewById(R.id.date_time_alarm_group);
+ mDateTimeAlarmGroup.findViewById(R.id.empty_time_view).setVisibility(View.GONE);
mDateTimeGroup = (ViewGroup) findViewById(R.id.date_time_group);
- mDateTimeGroup.findViewById(R.id.empty_time_view).setVisibility(View.GONE);
+
mExpandedGroup = (ViewGroup) findViewById(R.id.expanded_group);
mHeaderQsPanel = (QuickQSPanel) findViewById(R.id.quick_qs_panel);
@@ -91,6 +98,7 @@
mSettingsContainer = findViewById(R.id.settings_button_container);
mSettingsButton.setOnClickListener(this);
+ mAlarmStatusCollapsed = (TextView) findViewById(R.id.alarm_status_collapsed);
mAlarmStatus = (TextView) findViewById(R.id.alarm_status);
mAlarmStatus.setOnClickListener(this);
@@ -110,6 +118,29 @@
getHeight()));
}
});
+ updateResources();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateResources();
+ }
+
+ private void updateResources() {
+ FontSizeUtils.updateFontSize(mAlarmStatus, R.dimen.qs_date_collapsed_size);
+ FontSizeUtils.updateFontSize(mEmergencyOnly, R.dimen.qs_emergency_calls_only_text_size);
+
+ mDateTimeTranslation = mContext.getResources().getDimension(
+ R.dimen.qs_date_anim_translation);
+ mDateTimeAlarmTranslation = mContext.getResources().getDimension(
+ R.dimen.qs_date_alarm_anim_translation);
+ float dateCollapsedSize = mContext.getResources().getDimension(
+ R.dimen.qs_date_collapsed_text_size);
+ float dateExpandedSize = mContext.getResources().getDimension(
+ R.dimen.qs_date_text_size);
+ mDateScaleFactor = dateExpandedSize / dateCollapsedSize - 1;
+ updateDateTimePosition();
}
@Override
@@ -140,15 +171,41 @@
@Override
public void setExpansion(float headerExpansionFraction) {
+ mExpansionFraction = headerExpansionFraction;
+
mExpandedGroup.setAlpha(headerExpansionFraction);
mExpandedGroup.setVisibility(headerExpansionFraction > 0 ? View.VISIBLE : View.INVISIBLE);
+
mHeaderQsPanel.setAlpha(1 - headerExpansionFraction);
mHeaderQsPanel.setVisibility(headerExpansionFraction < 1 ? View.VISIBLE : View.INVISIBLE);
- mDateTimeGroup.setTranslationY(headerExpansionFraction * mDateTimeTranslation);
+ mAlarmStatus.setAlpha(headerExpansionFraction);
+ mAlarmStatusCollapsed.setAlpha(1 - headerExpansionFraction);
+ updateAlarmVisibilities();
+
+ float textScale = headerExpansionFraction * mDateScaleFactor;
+ mDateTimeGroup.setScaleX(1 + textScale);
+ mDateTimeGroup.setScaleY(1 + textScale);
+ mDateTimeGroup.setTranslationX(textScale * mDateTimeGroup.getWidth() / 2);
+ mDateTimeGroup.setTranslationY(textScale * mDateTimeGroup.getHeight() / 2);
+ updateDateTimePosition();
+
mEmergencyOnly.setAlpha(headerExpansionFraction);
}
+ private void updateAlarmVisibilities() {
+ mAlarmStatus.setVisibility(mAlarmShowing && mExpansionFraction > 0
+ ? View.VISIBLE : View.INVISIBLE);
+ mAlarmStatusCollapsed.setVisibility(mAlarmShowing && mExpansionFraction < 1
+ ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void updateDateTimePosition() {
+ float translation = mAlarmShowing ? mDateTimeAlarmTranslation
+ : mDateTimeTranslation;
+ mDateTimeAlarmGroup.setTranslationY(mExpansionFraction * translation);
+ }
+
public void setListening(boolean listening) {
if (listening == mListening) {
return;
@@ -160,11 +217,12 @@
@Override
public void updateEverything() {
+ updateDateTimePosition();
updateVisibilities();
}
private void updateVisibilities() {
- mAlarmStatus.setVisibility(mAlarmShowing ? View.VISIBLE : View.GONE);
+ updateAlarmVisibilities();
mEmergencyOnly.setVisibility(mExpanded && mShowEmergencyCallsOnly
? View.VISIBLE : View.INVISIBLE);
mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility(
diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipOverlayActivity.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipOverlayActivity.java
index b407935..7b1764f 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipOverlayActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipOverlayActivity.java
@@ -50,8 +50,9 @@
}
@Override
- protected void onStart() {
- super.onStart();
+ protected void onResume() {
+ super.onResume();
+ mGuideOverlayView.setVisibility(View.VISIBLE);
mHandler.removeCallbacks(mHideGuideOverlayRunnable);
mHandler.postDelayed(mHideGuideOverlayRunnable, SHOW_GUIDE_OVERLAY_VIEW_DURATION_MS);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 3335315..3e7466f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -275,6 +275,9 @@
private void processBatchedEvents(long frameNanos) {
MotionEventHolder current = mEventQueue;
+ if (current == null) {
+ return;
+ }
while (current.next != null) {
current = current.next;
}
@@ -403,6 +406,9 @@
}
private void disableFeatures() {
+ // Give the features a chance to process any batched events so we'll keep a consistent
+ // event stream
+ processBatchedEvents(Long.MAX_VALUE);
if (mMotionEventInjector != null) {
mAms.setMotionEventInjector(null);
mMotionEventInjector.onDestroy();
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index f522288..7770d53 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -3004,6 +3004,25 @@
}
}
+ // TODO: The following code should find better place to live.
+ if (!resetDefaultEnabledIme) {
+ boolean enabledImeFound = false;
+ final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked();
+ final int N = enabledImes.size();
+ for (int i = 0; i < N; ++i) {
+ final InputMethodInfo imi = enabledImes.get(i);
+ if (mMethodList.contains(imi)) {
+ enabledImeFound = true;
+ break;
+ }
+ }
+ if (!enabledImeFound) {
+ Slog.i(TAG, "All the enabled IMEs are gone. Reset default enabled IMEs.");
+ resetDefaultEnabledIme = true;
+ resetSelectedInputMethodAndSubtypeLocked("");
+ }
+ }
+
if (resetDefaultEnabledIme) {
final ArrayList<InputMethodInfo> defaultEnabledIme =
InputMethodUtils.getDefaultEnabledImes(mContext, mSystemReady, mMethodList);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ccaa1d2..c7f7378 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -6430,6 +6430,9 @@
if (mLockScreenShown == LOCK_SCREEN_SHOWN) {
mLockScreenShown = LOCK_SCREEN_HIDDEN;
updateSleepIfNeededLocked();
+
+ // Some stack visibility might change (e.g. docked stack)
+ mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
}
}
} finally {
diff --git a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
index 5806f3f..a6325a4 100644
--- a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
@@ -20,6 +20,7 @@
import android.media.AudioRecordConfiguration;
import android.media.AudioSystem;
import android.media.IRecordingConfigDispatcher;
+import android.media.MediaRecorder;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -48,6 +49,9 @@
* Implementation of android.media.AudioSystem.AudioRecordingCallback
*/
public void onRecordingConfigurationChanged(int event, int session, int source) {
+ if (MediaRecorder.isSystemOnlyAudioSource(source)) {
+ return;
+ }
if (updateSnapshot(event, session, source)) {
final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
synchronized(mClients) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 5db7e63..cc5b80e 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -65,6 +65,7 @@
import static android.content.pm.PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+import static android.content.pm.PackageManager.MOVE_FAILED_DEVICE_ADMIN;
import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST;
import static android.content.pm.PackageManager.MOVE_FAILED_INTERNAL_ERROR;
import static android.content.pm.PackageManager.MOVE_FAILED_OPERATION_PENDING;
@@ -14104,6 +14105,11 @@
});
}
+ @Override
+ public boolean isPackageDeviceAdminOnAnyUser(String packageName) {
+ return isPackageDeviceAdmin(packageName, UserHandle.USER_ALL);
+ }
+
private boolean isPackageDeviceAdmin(String packageName, int userId) {
IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
@@ -18170,6 +18176,10 @@
throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
"Package already moved to " + volumeUuid);
}
+ if (pkg.applicationInfo.isInternal() && isPackageDeviceAdminOnAnyUser(packageName)) {
+ throw new PackageManagerException(MOVE_FAILED_DEVICE_ADMIN,
+ "Device admin cannot be moved");
+ }
if (ps.frozen) {
throw new PackageManagerException(MOVE_FAILED_OPERATION_PENDING,
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 6c2e4d4..e88b72f 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2409,7 +2409,6 @@
case TYPE_WALLPAPER:
case TYPE_DREAM:
case TYPE_KEYGUARD_SCRIM:
- case TYPE_DOCK_DIVIDER:
return false;
default:
// Hide only windows below the keyguard host window.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 0c429e5..144d7ac 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -19,6 +19,9 @@
import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
import static android.app.ActivityManager.StackId.HOME_STACK_ID;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowState.RESIZE_HANDLE_WIDTH_IN_DP;
@@ -607,4 +610,39 @@
final TaskStack stack = mService.mStackIdToStack.get(DOCKED_STACK_ID);
return (stack != null && stack.isVisibleLocked()) ? stack : null;
}
+
+ /**
+ * Find the visible, touch-deliverable window under the given point
+ */
+ WindowState getTouchableWinAtPointLocked(float xf, float yf) {
+ WindowState touchedWin = null;
+ final int x = (int) xf;
+ final int y = (int) yf;
+
+ for (int i = mWindows.size() - 1; i >= 0; i--) {
+ WindowState window = mWindows.get(i);
+ final int flags = window.mAttrs.flags;
+ if (!window.isVisibleLw()) {
+ continue;
+ }
+ if ((flags & FLAG_NOT_TOUCHABLE) != 0) {
+ continue;
+ }
+
+ window.getVisibleBounds(mTmpRect);
+ if (!mTmpRect.contains(x, y)) {
+ continue;
+ }
+
+ window.getTouchableRegion(mTmpRegion);
+
+ final int touchFlags = flags & (FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL);
+ if (mTmpRegion.contains(x, y) || touchFlags == 0) {
+ touchedWin = window;
+ break;
+ }
+ }
+
+ return touchedWin;
+ }
}
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 685403c..75c06ff 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -102,7 +102,9 @@
return;
}
TaskStack stack = mDisplayContent.mService.mStackIdToStack.get(DOCKED_STACK_ID);
- final boolean visible = stack != null && stack.isVisibleLocked();
+
+ // If the stack is invisible, we policy force hide it in WindowAnimator.shouldForceHide
+ final boolean visible = stack != null;
if (mLastVisibility == visible && !force) {
return;
}
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 9a3aaa5..cf27b97 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -27,8 +27,6 @@
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.IBinder;
import android.os.Message;
@@ -71,6 +69,13 @@
class DragState {
private static final long ANIMATION_DURATION_MS = 500;
+ private static final int DRAG_FLAGS_URI_ACCESS = View.DRAG_FLAG_GLOBAL_URI_READ |
+ View.DRAG_FLAG_GLOBAL_URI_WRITE;
+
+ private static final int DRAG_FLAGS_URI_PERMISSIONS = DRAG_FLAGS_URI_ACCESS |
+ View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION |
+ View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION;
+
final WindowManagerService mService;
IBinder mToken;
SurfaceControl mSurfaceControl;
@@ -95,10 +100,7 @@
WindowState mTargetWindow;
ArrayList<WindowState> mNotifiedWindows;
boolean mDragInProgress;
- Display mDisplay;
-
- private final Region mTmpRegion = new Region();
- private final Rect mTmpRect = new Rect();
+ DisplayContent mDisplayContent;
private Animation mAnimation;
final Transformation mTransformation = new Transformation();
@@ -131,11 +133,12 @@
* @param display The Display that the window being dragged is on.
*/
void register(Display display) {
- mDisplay = display;
if (DEBUG_DRAG) Slog.d(TAG_WM, "registering drag input channel");
if (mClientChannel != null) {
Slog.e(TAG_WM, "Duplicate register of drag input channel");
} else {
+ mDisplayContent = mService.getDisplayContentLocked(display.getDisplayId());
+
InputChannel[] channels = InputChannel.openInputChannelPair("drag");
mServerChannel = channels[0];
mClientChannel = channels[1];
@@ -149,7 +152,7 @@
WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS;
mDragWindowHandle = new InputWindowHandle(mDragApplicationHandle, null,
- mDisplay.getDisplayId());
+ display.getDisplayId());
mDragWindowHandle.name = "drag";
mDragWindowHandle.inputChannel = mServerChannel;
mDragWindowHandle.layer = getDragLayerLw();
@@ -174,7 +177,7 @@
mDragWindowHandle.frameLeft = 0;
mDragWindowHandle.frameTop = 0;
Point p = new Point();
- mDisplay.getRealSize(p);
+ display.getRealSize(p);
mDragWindowHandle.frameRight = p.x;
mDragWindowHandle.frameBottom = p.y;
@@ -244,12 +247,10 @@
Slog.d(TAG_WM, "broadcasting DRAG_STARTED at (" + touchX + ", " + touchY + ")");
}
- final WindowList windows = mService.getWindowListLocked(mDisplay);
- if (windows != null) {
- final int N = windows.size();
- for (int i = 0; i < N; i++) {
- sendDragStartedLw(windows.get(i), touchX, touchY, mDataDescription);
- }
+ final WindowList windows = mDisplayContent.getWindowList();
+ final int N = windows.size();
+ for (int i = 0; i < N; i++) {
+ sendDragStartedLw(windows.get(i), touchX, touchY, mDataDescription);
}
}
@@ -379,7 +380,7 @@
private void cleanUpDragLw() {
broadcastDragEndedLw();
if (isFromSource(InputDevice.SOURCE_MOUSE)) {
- mService.restorePointerIconLocked(mDisplay, mCurrentX, mCurrentY);
+ mService.restorePointerIconLocked(mDisplayContent, mCurrentX, mCurrentY);
}
// stop intercepting input
@@ -418,7 +419,7 @@
void notifyLocationLw(float x, float y) {
// Tell the affected window
- WindowState touchedWin = mService.getTouchableWinAtPointLocked(mDisplay, x, y);
+ WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
if (touchedWin == null) {
if (DEBUG_DRAG) Slog.d(TAG_WM, "No touched win at x=" + x + " y=" + y);
return;
@@ -463,17 +464,18 @@
mTargetWindow = touchedWin;
}
- // Tell the drop target about the data. Returns 'true' if we can immediately
+ // Find the drop target and tell it about the data. Returns 'true' if we can immediately
// dispatch the global drag-ended message, 'false' if we need to wait for a
// result from the recipient.
- boolean notifyDropLw(WindowState touchedWin, IDropPermissions dropPermissions,
- float x, float y) {
+ boolean notifyDropLw(float x, float y) {
if (mAnimation != null) {
return false;
}
mCurrentX = x;
mCurrentY = y;
+ WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
+
if (!isWindowNotified(touchedWin)) {
// "drop" outside a valid window -- no recipient to apply a
// timeout to, and we can send the drag-ended message immediately.
@@ -484,7 +486,21 @@
if (DEBUG_DRAG) {
Slog.d(TAG_WM, "sending DROP to " + touchedWin);
}
- if (mSourceUserId != UserHandle.getUserId(touchedWin.getOwningUid())){
+
+ final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
+
+ DropPermissionsHandler dropPermissions = null;
+ if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 &&
+ (mFlags & DRAG_FLAGS_URI_ACCESS) != 0) {
+ dropPermissions = new DropPermissionsHandler(
+ mData,
+ mUid,
+ touchedWin.getOwningPackage(),
+ mFlags & DRAG_FLAGS_URI_PERMISSIONS,
+ mSourceUserId,
+ targetUserId);
+ }
+ if (mSourceUserId != targetUserId){
mData.fixUris(mSourceUserId);
}
final int myPid = Process.myPid();
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index d169b34..8409058 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -898,7 +898,8 @@
}
boolean isVisibleLocked() {
- final boolean keyguardOn = mService.mPolicy.isKeyguardShowingOrOccluded();
+ final boolean keyguardOn = mService.mPolicy.isKeyguardShowingOrOccluded()
+ && !mService.mAnimator.mKeyguardGoingAway;
if (keyguardOn && !StackId.isAllowedOverLockscreen(mStackId)) {
// The keyguard is showing and the stack shouldn't show on top of the keyguard.
return false;
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 8d2fb9b..f8a4d33 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS_LIGHT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_KEYGUARD;
@@ -231,7 +232,10 @@
// Only hide windows if the keyguard is active and not animating away.
boolean keyguardOn = mPolicy.isKeyguardShowingOrOccluded()
&& mForceHiding != KEYGUARD_ANIMATING_OUT;
- return keyguardOn && !allowWhenLocked && (win.getDisplayId() == Display.DEFAULT_DISPLAY);
+ boolean hideDockDivider = win.mAttrs.type == TYPE_DOCK_DIVIDER
+ && win.getDisplayContent().getDockedStackLocked() == null;
+ return keyguardOn && !allowWhenLocked && (win.getDisplayId() == Display.DEFAULT_DISPLAY)
+ || hideDockDivider;
}
private void updateWindowsLocked(final int displayId) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5cd14c9..7169375 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -664,13 +664,6 @@
private WindowContentFrameStats mTempWindowRenderStats;
- private static final int DRAG_FLAGS_URI_ACCESS = View.DRAG_FLAG_GLOBAL_URI_READ |
- View.DRAG_FLAG_GLOBAL_URI_WRITE;
-
- private static final int DRAG_FLAGS_URI_PERMISSIONS = DRAG_FLAGS_URI_ACCESS |
- View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION |
- View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION;
-
final class DragInputEventReceiver extends InputEventReceiver {
// Set, if stylus button was down at the start of the drag.
private boolean mStylusButtonDownAtStart;
@@ -716,7 +709,7 @@
if (DEBUG_DRAG) Slog.d(TAG_WM, "Button no longer pressed; dropping at "
+ newX + "," + newY);
synchronized (mWindowMap) {
- endDrag = completeDropLw(newX, newY);
+ endDrag = mDragState.notifyDropLw(newX, newY);
}
} else {
synchronized (mWindowMap) {
@@ -730,7 +723,7 @@
if (DEBUG_DRAG) Slog.d(TAG_WM, "Got UP on move channel; dropping at "
+ newX + "," + newY);
synchronized (mWindowMap) {
- endDrag = completeDropLw(newX, newY);
+ endDrag = mDragState.notifyDropLw(newX, newY);
}
} break;
@@ -760,25 +753,6 @@
}
}
- private boolean completeDropLw(float x, float y) {
- WindowState dropTargetWin = getTouchableWinAtPointLocked(mDragState.mDisplay, x, y);
-
- DropPermissionsHandler dropPermissions = null;
- if (dropTargetWin != null &&
- (mDragState.mFlags & View.DRAG_FLAG_GLOBAL) != 0 &&
- (mDragState.mFlags & DRAG_FLAGS_URI_ACCESS) != 0) {
- dropPermissions = new DropPermissionsHandler(
- mDragState.mData,
- mDragState.mUid,
- dropTargetWin.getOwningPackage(),
- mDragState.mFlags & DRAG_FLAGS_URI_PERMISSIONS,
- mDragState.mSourceUserId,
- UserHandle.getUserId(dropTargetWin.getOwningUid()));
- }
-
- return mDragState.notifyDropLw(dropTargetWin, dropPermissions, x, y);
- }
-
/**
* Whether the UI is currently running in touch mode (not showing
* navigational focus because the user is directly pressing the screen).
@@ -10467,43 +10441,6 @@
}
}
- /**
- * Find the visible, touch-deliverable window under the given point
- */
- WindowState getTouchableWinAtPointLocked(Display display, float xf, float yf) {
- WindowState touchedWin = null;
- final int x = (int) xf;
- final int y = (int) yf;
-
- final WindowList windows = getWindowListLocked(display);
- if (windows == null) {
- return null;
- }
- final int N = windows.size();
- for (int i = N - 1; i >= 0; i--) {
- WindowState child = windows.get(i);
- final int flags = child.mAttrs.flags;
- if (!child.isVisibleLw()) {
- continue;
- }
- if ((flags & WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) != 0) {
- continue;
- }
-
- child.getTouchableRegion(mTmpRegion);
-
- final int touchFlags = flags &
- (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
- if (mTmpRegion.contains(x, y) || touchFlags == 0) {
- touchedWin = child;
- break;
- }
- }
-
- return touchedWin;
- }
-
private MousePositionTracker mMousePositionTracker = new MousePositionTracker();
private static class MousePositionTracker implements PointerEventListener {
@@ -10556,8 +10493,8 @@
if (displayContent == null) {
return;
}
- Display display = displayContent.getDisplay();
- WindowState windowUnderPointer = getTouchableWinAtPointLocked(display, mouseX, mouseY);
+ WindowState windowUnderPointer =
+ displayContent.getTouchableWinAtPointLocked(mouseX, mouseY);
if (windowUnderPointer != callingWin) {
return;
}
@@ -10571,11 +10508,12 @@
}
}
- void restorePointerIconLocked(Display display, float latestX, float latestY) {
+ void restorePointerIconLocked(DisplayContent displayContent, float latestX, float latestY) {
// Mouse position tracker has not been getting updates while dragging, update it now.
mMousePositionTracker.updatePosition(latestX, latestY);
- WindowState windowUnderPointer = getTouchableWinAtPointLocked(display, latestX, latestY);
+ WindowState windowUnderPointer =
+ displayContent.getTouchableWinAtPointLocked(latestX, latestY);
if (windowUnderPointer != null) {
try {
windowUnderPointer.mClient.updatePointerIcon(
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fa727d4..79d2307 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2702,6 +2702,10 @@
if (info == null) {
throw new IllegalArgumentException("Bad admin: " + adminReceiver);
}
+ if (!info.getActivityInfo().applicationInfo.isInternal()) {
+ throw new IllegalArgumentException("Only apps in internal storage can be active admin: "
+ + adminReceiver);
+ }
synchronized (this) {
long ident = mInjector.binderClearCallingIdentity();
try {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 46ad8a1..acc752a 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -74,6 +74,7 @@
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.SystemService;
@@ -125,6 +126,7 @@
Handler mHandler;
AppOpsManager mAppOps;
UserManager mUserManager;
+ PackageManager mPackageManager;
AppWidgetManager mAppWidgetManager;
IDeviceIdleController mDeviceIdleController;
private DisplayManager mDisplayManager;
@@ -157,7 +159,7 @@
public void onStart() {
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
-
+ mPackageManager = getContext().getPackageManager();
mHandler = new H(BackgroundThread.get().getLooper());
File systemDataDir = new File(Environment.getDataDirectory(), "system");
@@ -296,9 +298,8 @@
private void initializeDefaultsForSystemApps(int userId) {
Slog.d(TAG, "Initializing defaults for system apps on user " + userId);
final long elapsedRealtime = SystemClock.elapsedRealtime();
- List<PackageInfo> packages = getContext().getPackageManager().getInstalledPackagesAsUser(
- PackageManager.MATCH_DISABLED_COMPONENTS
- | PackageManager.MATCH_UNINSTALLED_PACKAGES,
+ List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
+ PackageManager.MATCH_DISABLED_COMPONENTS,
userId);
final int packageCount = packages.size();
for (int i = 0; i < packageCount; i++) {
@@ -398,31 +399,38 @@
}
}
- /** Check all running users' or specified user's apps to see if they enter an idle state. */
- void checkIdleStates(int checkUserId) {
+ /**
+ * Check all running users' or specified user's apps to see if they enter an idle state.
+ * @return Returns whether checking should continue periodically.
+ */
+ boolean checkIdleStates(int checkUserId) {
if (!mAppIdleEnabled) {
- return;
+ return false;
}
- final int[] userIds;
+ final int[] runningUserIds;
try {
- if (checkUserId == UserHandle.USER_ALL) {
- userIds = ActivityManagerNative.getDefault().getRunningUserIds();
- } else {
- userIds = new int[] { checkUserId };
+ runningUserIds = ActivityManagerNative.getDefault().getRunningUserIds();
+ if (checkUserId != UserHandle.USER_ALL
+ && !ArrayUtils.contains(runningUserIds, checkUserId)) {
+ return false;
}
} catch (RemoteException re) {
- return;
+ return false;
}
final long elapsedRealtime = SystemClock.elapsedRealtime();
- for (int i = 0; i < userIds.length; i++) {
- final int userId = userIds[i];
- List<PackageInfo> packages =
- getContext().getPackageManager().getInstalledPackagesAsUser(
- PackageManager.MATCH_DISABLED_COMPONENTS
- | PackageManager.MATCH_UNINSTALLED_PACKAGES,
- userId);
+ for (int i = 0; i < runningUserIds.length; i++) {
+ final int userId = runningUserIds[i];
+ if (checkUserId != UserHandle.USER_ALL && checkUserId != userId) {
+ continue;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Checking idle state for user " + userId);
+ }
+ List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
+ PackageManager.MATCH_DISABLED_COMPONENTS,
+ userId);
synchronized (mLock) {
final int packageCount = packages.size();
for (int p = 0; p < packageCount; p++) {
@@ -439,6 +447,11 @@
}
}
}
+ if (DEBUG) {
+ Slog.d(TAG, "checkIdleStates took "
+ + (SystemClock.elapsedRealtime() - elapsedRealtime));
+ }
+ return true;
}
/** Check if it's been a while since last parole and let idle apps do some work */
@@ -459,7 +472,7 @@
private void notifyBatteryStats(String packageName, int userId, boolean idle) {
try {
- final int uid = AppGlobals.getPackageManager().getPackageUid(packageName,
+ final int uid = mPackageManager.getPackageUidAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
if (idle) {
mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE,
@@ -468,7 +481,7 @@
mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE,
packageName, uid);
}
- } catch (RemoteException re) {
+ } catch (NameNotFoundException | RemoteException e) {
}
}
@@ -592,7 +605,7 @@
// Only force the sync adapters to active if the provider is not in the same package and
// the sync adapter is a system package.
try {
- PackageInfo pi = AppGlobals.getPackageManager().getPackageInfo(
+ PackageInfo pi = mPackageManager.getPackageInfoAsUser(
packageName, PackageManager.MATCH_SYSTEM_ONLY, userId);
if (pi == null || pi.applicationInfo == null) {
continue;
@@ -600,7 +613,7 @@
if (!packageName.equals(providerPkgName)) {
forceIdleState(packageName, userId, false);
}
- } catch (RemoteException re) {
+ } catch (NameNotFoundException e) {
// Shouldn't happen
}
}
@@ -725,7 +738,7 @@
int getAppId(String packageName) {
try {
- ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(packageName,
+ ApplicationInfo ai = mPackageManager.getApplicationInfo(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS);
return ai.uid;
@@ -772,12 +785,8 @@
}
} catch (RemoteException re) {
}
- // TODO: Optimize this check
- if (isActiveDeviceAdmin(packageName, userId)) {
- return false;
- }
- if (isCarrierApp(packageName)) {
+ if (isActiveDeviceAdmin(packageName, userId)) {
return false;
}
@@ -790,7 +799,17 @@
return false;
}
- return isAppIdleUnfiltered(packageName, userId, elapsedRealtime);
+ if (!isAppIdleUnfiltered(packageName, userId, elapsedRealtime)) {
+ return false;
+ }
+
+ // Check this last, as it is the most expensive check
+ // TODO: Optimize this by fetching the carrier privileged apps ahead of time
+ if (isCarrierApp(packageName)) {
+ return false;
+ }
+
+ return true;
}
int[] getIdleUidsForUser(int userId) {
@@ -803,7 +822,7 @@
List<ApplicationInfo> apps;
try {
ParceledListSlice<ApplicationInfo> slice = AppGlobals.getPackageManager()
- .getInstalledApplications(PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+ .getInstalledApplications(/* flags= */ 0, userId);
if (slice == null) {
return new int[0];
}
@@ -833,7 +852,9 @@
uidStates.setValueAt(index, value + 1 + (idle ? 1<<16 : 0));
}
}
-
+ if (DEBUG) {
+ Slog.d(TAG, "getIdleUids took " + (SystemClock.elapsedRealtime() - elapsedRealtime));
+ }
int numIdle = 0;
for (int i = uidStates.size() - 1; i >= 0; i--) {
int value = uidStates.valueAt(i);
@@ -865,15 +886,7 @@
private boolean isActiveDeviceAdmin(String packageName, int userId) {
DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
if (dpm == null) return false;
- List<ComponentName> components = dpm.getActiveAdminsAsUser(userId);
- if (components == null) return false;
- final int size = components.size();
- for (int i = 0; i < size; i++) {
- if (components.get(i).getPackageName().equals(packageName)) {
- return true;
- }
- }
- return false;
+ return dpm.packageHasActiveAdmins(packageName, userId);
}
private boolean isCarrierApp(String packageName) {
@@ -1011,10 +1024,11 @@
break;
case MSG_CHECK_IDLE_STATES:
- checkIdleStates(msg.arg1);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- MSG_CHECK_IDLE_STATES, msg.arg1, 0),
- mCheckIdleIntervalMillis);
+ if (checkIdleStates(msg.arg1)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_CHECK_IDLE_STATES, msg.arg1, 0),
+ mCheckIdleIntervalMillis);
+ }
break;
case MSG_ONE_TIME_CHECK_IDLE_STATES:
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
index 18a5d59..f7cd6a3 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java
@@ -54,6 +54,7 @@
private static final String CREATE_TABLE_ST_SOUND_MODEL = "CREATE TABLE "
+ GenericSoundModelContract.TABLE + "("
+ GenericSoundModelContract.KEY_MODEL_UUID + " TEXT PRIMARY KEY,"
+ + GenericSoundModelContract.KEY_VENDOR_UUID + " TEXT,"
+ GenericSoundModelContract.KEY_DATA + " BLOB" + " )";
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index 354075e..cde47bd 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -16,12 +16,16 @@
package com.android.server.soundtrigger;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.GenericRecognitionEvent;
+import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
@@ -29,6 +33,7 @@
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
import android.hardware.soundtrigger.SoundTrigger.SoundModelEvent;
import android.hardware.soundtrigger.SoundTriggerModule;
import android.os.PowerManager;
@@ -40,9 +45,16 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.UUID;
/**
- * Helper for {@link SoundTrigger} APIs.
+ * Helper for {@link SoundTrigger} APIs. Supports two types of models:
+ * (i) A voice model which is exported via the {@link VoiceInteractionService}. There can only be
+ * a single voice model running on the DSP at any given time.
+ *
+ * (ii) Generic sound-trigger models: Supports multiple of these.
+ *
* Currently this just acts as an abstraction over all SoundTrigger API calls.
*
* @hide
@@ -62,7 +74,7 @@
private static final int INVALID_VALUE = Integer.MIN_VALUE;
/** The {@link ModuleProperties} for the system, or null if none exists. */
- final ModuleProperties moduleProperties;
+ final ModuleProperties mModuleProperties;
/** The properties for the DSP module */
private SoundTriggerModule mModule;
@@ -72,21 +84,36 @@
private final PhoneStateListener mPhoneStateListener;
private final PowerManager mPowerManager;
- // TODO: Since many layers currently only deal with one recognition
+ // TODO: Since the voice layer currently only handles one recognition
// we simplify things by assuming one listener here too.
- private IRecognitionStatusCallback mActiveListener;
+ private IRecognitionStatusCallback mKeyphraseListener;
+
+ // The SoundTriggerManager layer handles multiple generic recognition models. We store the
+ // ModelData here in a hashmap.
+ private final HashMap<UUID, ModelData> mGenericModelDataMap;
+
+ // Note: KeyphraseId is not really used.
private int mKeyphraseId = INVALID_VALUE;
- private int mCurrentSoundModelHandle = INVALID_VALUE;
+
+ // Current voice sound model handle. We only allow one voice model to run at any given time.
+ private int mCurrentKeyphraseModelHandle = INVALID_VALUE;
private KeyphraseSoundModel mCurrentSoundModel = null;
// FIXME: Ideally this should not be stored if allowMultipleTriggers happens at a lower layer.
private RecognitionConfig mRecognitionConfig = null;
+
+ // Whether we are requesting recognition to start.
private boolean mRequested = false;
private boolean mCallActive = false;
private boolean mIsPowerSaveMode = false;
// Indicates if the native sound trigger service is disabled or not.
// This is an indirect indication of the microphone being open in some other application.
private boolean mServiceDisabled = false;
- private boolean mStarted = false;
+
+ // Whether we have ANY recognition (keyphrase or generic) running.
+ private boolean mRecognitionRunning = false;
+
+ // Keeps track of whether the keyphrase recognition is running.
+ private boolean mKeyphraseStarted = false;
private boolean mRecognitionAborted = false;
private PowerSaveModeListener mPowerSaveModeListener;
@@ -96,14 +123,87 @@
mContext = context;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mGenericModelDataMap = new HashMap<UUID, ModelData>();
mPhoneStateListener = new MyCallStateListener();
if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size());
- moduleProperties = null;
+ mModuleProperties = null;
mModule = null;
} else {
// TODO: Figure out how to determine which module corresponds to the DSP hardware.
- moduleProperties = modules.get(0);
+ mModuleProperties = modules.get(0);
+ }
+ }
+
+ /**
+ * Starts recognition for the given generic sound model ID.
+ *
+ * @param soundModel The sound model to use for recognition.
+ * @param listener The listener for the recognition events related to the given keyphrase.
+ * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ */
+ int startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
+ IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
+ if (soundModel == null || callback == null || recognitionConfig == null) {
+ Slog.w(TAG, "Passed in bad data to startGenericRecognition().");
+ return STATUS_ERROR;
+ }
+
+ synchronized (mLock) {
+
+ if (mModuleProperties == null) {
+ Slog.w(TAG, "Attempting startRecognition without the capability");
+ return STATUS_ERROR;
+ }
+
+ if (mModule == null) {
+ mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
+ if (mModule == null) {
+ Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
+ return STATUS_ERROR;
+ }
+ }
+
+ // Initialize power save, call active state monitoring logic.
+ if (!mRecognitionRunning) {
+ initializeTelephonyAndPowerStateListeners();
+ }
+
+ // Fetch a ModelData instance from the hash map. Creates a new one if none
+ // exists.
+ ModelData modelData = getOrCreateGenericModelData(modelId);
+
+ IRecognitionStatusCallback oldCallback = modelData.getCallback();
+ if (oldCallback != null) {
+ Slog.w(TAG, "Canceling previous recognition for model id: " + modelId);
+ try {
+ oldCallback.onError(STATUS_ERROR);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onDetectionStopped", e);
+ }
+ modelData.clearCallback();
+ }
+
+ // Load the model if its not loaded.
+ if (!modelData.isModelLoaded()) {
+ // Load the model
+ int[] handle = new int[] { INVALID_VALUE };
+ int status = mModule.loadSoundModel(soundModel, handle);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "loadSoundModel call failed with " + status);
+ return status;
+ }
+ if (handle[0] == INVALID_VALUE) {
+ Slog.w(TAG, "loadSoundModel call returned invalid sound model handle");
+ return STATUS_ERROR;
+ }
+ modelData.setHandle(handle[0]);
+ }
+ modelData.setCallback(callback);
+ modelData.setRecognitionConfig(recognitionConfig);
+
+ // Don't notify for synchronous calls.
+ return startGenericRecognitionLocked(modelData, false);
}
}
@@ -116,7 +216,7 @@
* @param listener The listener for the recognition events related to the given keyphrase.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int startRecognition(int keyphraseId,
+ int startKeyphraseRecognition(int keyphraseId,
KeyphraseSoundModel soundModel,
IRecognitionStatusCallback listener,
RecognitionConfig recognitionConfig) {
@@ -129,36 +229,24 @@
Slog.d(TAG, "startRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", listener=" + listener.asBinder()
+ ", recognitionConfig=" + recognitionConfig);
- Slog.d(TAG, "moduleProperties=" + moduleProperties);
+ Slog.d(TAG, "moduleProperties=" + mModuleProperties);
Slog.d(TAG, "current listener="
- + (mActiveListener == null ? "null" : mActiveListener.asBinder()));
- Slog.d(TAG, "current SoundModel handle=" + mCurrentSoundModelHandle);
+ + (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
+ Slog.d(TAG, "current SoundModel handle=" + mCurrentKeyphraseModelHandle);
Slog.d(TAG, "current SoundModel UUID="
+ (mCurrentSoundModel == null ? null : mCurrentSoundModel.uuid));
}
- if (!mStarted) {
- // Get the current call state synchronously for the first recognition.
- mCallActive = mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
- // Register for call state changes when the first call to start recognition occurs.
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
-
- // Register for power saver mode changes when the first call to start recognition
- // occurs.
- if (mPowerSaveModeListener == null) {
- mPowerSaveModeListener = new PowerSaveModeListener();
- mContext.registerReceiver(mPowerSaveModeListener,
- new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
- }
- mIsPowerSaveMode = mPowerManager.isPowerSaveMode();
+ if (!mRecognitionRunning) {
+ initializeTelephonyAndPowerStateListeners();
}
- if (moduleProperties == null) {
+ if (mModuleProperties == null) {
Slog.w(TAG, "Attempting startRecognition without the capability");
return STATUS_ERROR;
}
if (mModule == null) {
- mModule = SoundTrigger.attachModule(moduleProperties.id, this, null);
+ mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
if (mModule == null) {
Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
return STATUS_ERROR;
@@ -168,32 +256,32 @@
// Unload the previous model if the current one isn't invalid
// and, it's not the same as the new one.
// This helps use cache and reuse the model and just start/stop it when necessary.
- if (mCurrentSoundModelHandle != INVALID_VALUE
+ if (mCurrentKeyphraseModelHandle != INVALID_VALUE
&& !soundModel.equals(mCurrentSoundModel)) {
Slog.w(TAG, "Unloading previous sound model");
- int status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
+ int status = mModule.unloadSoundModel(mCurrentKeyphraseModelHandle);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "unloadSoundModel call failed with " + status);
}
- internalClearSoundModelLocked();
- mStarted = false;
+ internalClearKeyphraseSoundModelLocked();
+ mKeyphraseStarted = false;
}
// If the previous recognition was by a different listener,
// Notify them that it was stopped.
- if (mActiveListener != null && mActiveListener.asBinder() != listener.asBinder()) {
+ if (mKeyphraseListener != null && mKeyphraseListener.asBinder() != listener.asBinder()) {
Slog.w(TAG, "Canceling previous recognition");
try {
- mActiveListener.onError(STATUS_ERROR);
+ mKeyphraseListener.onError(STATUS_ERROR);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetectionStopped", e);
}
- mActiveListener = null;
+ mKeyphraseListener = null;
}
// Load the sound model if the current one is null.
- int soundModelHandle = mCurrentSoundModelHandle;
- if (mCurrentSoundModelHandle == INVALID_VALUE
+ int soundModelHandle = mCurrentKeyphraseModelHandle;
+ if (mCurrentKeyphraseModelHandle == INVALID_VALUE
|| mCurrentSoundModel == null) {
int[] handle = new int[] { INVALID_VALUE };
int status = mModule.loadSoundModel(soundModel, handle);
@@ -213,18 +301,81 @@
// Start the recognition.
mRequested = true;
mKeyphraseId = keyphraseId;
- mCurrentSoundModelHandle = soundModelHandle;
+ mCurrentKeyphraseModelHandle = soundModelHandle;
mCurrentSoundModel = soundModel;
mRecognitionConfig = recognitionConfig;
// Register the new listener. This replaces the old one.
// There can only be a maximum of one active listener at any given time.
- mActiveListener = listener;
+ mKeyphraseListener = listener;
return updateRecognitionLocked(false /* don't notify for synchronous calls */);
}
}
/**
+ * Stops recognition for the given generic sound model.
+ *
+ * @param modelId The identifier of the generic sound model for which
+ * the recognition is to be stopped.
+ * @param listener The listener for the recognition events related to the given sound model.
+ *
+ * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
+ */
+ int stopGenericRecognition(UUID modelId, IRecognitionStatusCallback listener) {
+ if (listener == null) {
+ return STATUS_ERROR;
+ }
+
+ synchronized (mLock) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData == null) {
+ Slog.w(TAG, "Attempting stopRecognition on invalid model with id:" + modelId);
+ return STATUS_ERROR;
+ }
+
+ IRecognitionStatusCallback currentCallback = modelData.getCallback();
+ if (DBG) {
+ Slog.d(TAG, "stopRecognition for modelId=" + modelId
+ + ", listener=" + listener.asBinder());
+ Slog.d(TAG, "current callback ="
+ + (currentCallback == null ? "null" : currentCallback.asBinder()));
+ }
+
+ if (mModuleProperties == null || mModule == null) {
+ Slog.w(TAG, "Attempting stopRecognition without the capability");
+ return STATUS_ERROR;
+ }
+
+ if (currentCallback == null || !modelData.modelStarted()) {
+ // startRecognition hasn't been called or it failed.
+ Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
+ return STATUS_ERROR;
+ }
+ if (currentCallback.asBinder() != listener.asBinder()) {
+ // We don't allow a different listener to stop the recognition than the one
+ // that started it.
+ Slog.w(TAG, "Attempting stopRecognition for another recognition");
+ return STATUS_ERROR;
+ }
+
+ int status = stopGenericRecognitionLocked(modelData, false /* don't notify for synchronous calls */);
+ if (status != SoundTrigger.STATUS_OK) {
+ return status;
+ }
+
+ // We leave the sound model loaded but not started, this helps us when we start
+ // back.
+ // Also clear the internal state once the recognition has been stopped.
+ modelData.clearState();
+ modelData.clearCallback();
+ if (!computeRecognitionRunning()) {
+ internalClearGlobalStateLocked();
+ }
+ return status;
+ }
+ }
+
+ /**
* Stops recognition for the given {@link Keyphrase} if a recognition is
* currently active.
*
@@ -234,7 +385,7 @@
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
+ int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
if (listener == null) {
return STATUS_ERROR;
}
@@ -244,20 +395,20 @@
Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId
+ ", listener=" + listener.asBinder());
Slog.d(TAG, "current listener="
- + (mActiveListener == null ? "null" : mActiveListener.asBinder()));
+ + (mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder()));
}
- if (moduleProperties == null || mModule == null) {
+ if (mModuleProperties == null || mModule == null) {
Slog.w(TAG, "Attempting stopRecognition without the capability");
return STATUS_ERROR;
}
- if (mActiveListener == null) {
+ if (mKeyphraseListener == null) {
// startRecognition hasn't been called or it failed.
Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
return STATUS_ERROR;
}
- if (mActiveListener.asBinder() != listener.asBinder()) {
+ if (mKeyphraseListener.asBinder() != listener.asBinder()) {
// We don't allow a different listener to stop the recognition than the one
// that started it.
Slog.w(TAG, "Attempting stopRecognition for another recognition");
@@ -274,7 +425,8 @@
// We leave the sound model loaded but not started, this helps us when we start
// back.
// Also clear the internal state once the recognition has been stopped.
- internalClearStateLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGlobalStateLocked();
return status;
}
}
@@ -284,38 +436,56 @@
*/
void stopAllRecognitions() {
synchronized (mLock) {
- if (moduleProperties == null || mModule == null) {
+ if (mModuleProperties == null || mModule == null) {
return;
}
- if (mCurrentSoundModelHandle == INVALID_VALUE) {
- return;
+ // Stop Keyphrase recognition if one exists.
+ if (mCurrentKeyphraseModelHandle != INVALID_VALUE) {
+
+ mRequested = false;
+ int status = updateRecognitionLocked(
+ false /* don't notify for synchronous calls */);
+ internalClearKeyphraseStateLocked();
}
- mRequested = false;
- int status = updateRecognitionLocked(false /* don't notify for synchronous calls */);
- internalClearStateLocked();
+ // Stop all generic recognition models.
+ for (ModelData model : mGenericModelDataMap.values()) {
+ if (model.modelStarted()) {
+ int status = stopGenericRecognitionLocked(model,
+ false /* do not notify for synchronous calls */);
+ if (status != STATUS_OK) {
+ // What else can we do if there is an error here.
+ Slog.w(TAG, "Error stopping generic model: " + model.getHandle());
+ }
+ model.clearState();
+ model.clearCallback();
+ }
+ }
+ internalClearGlobalStateLocked();
}
}
public ModuleProperties getModuleProperties() {
- return moduleProperties;
+ return mModuleProperties;
}
//---- SoundTrigger.StatusListener methods
@Override
public void onRecognition(RecognitionEvent event) {
- if (event == null || !(event instanceof KeyphraseRecognitionEvent)) {
- Slog.w(TAG, "Invalid recognition event!");
+ if (event == null) {
+ Slog.w(TAG, "Null recognition event!");
+ return;
+ }
+
+ if (!(event instanceof KeyphraseRecognitionEvent) &&
+ !(event instanceof GenericRecognitionEvent)) {
+ Slog.w(TAG, "Invalid recognition event type (not one of generic or keyphrase) !");
return;
}
if (DBG) Slog.d(TAG, "onRecognition: " + event);
synchronized (mLock) {
- if (mActiveListener == null) {
- Slog.w(TAG, "received onRecognition event without any listener for it");
- return;
- }
switch (event.status) {
// Fire aborts/failures to all listeners since it's not tied to a keyphrase.
case SoundTrigger.RECOGNITION_STATUS_ABORT:
@@ -325,12 +495,60 @@
onRecognitionFailureLocked();
break;
case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
- onRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
+
+ if (isKeyphraseRecognitionEvent(event)) {
+ onKeyphraseRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
+ } else {
+ onGenericRecognitionSuccessLocked((GenericRecognitionEvent) event);
+ }
+
break;
}
}
}
+ private boolean isKeyphraseRecognitionEvent(RecognitionEvent event) {
+ return mCurrentKeyphraseModelHandle == event.soundModelHandle;
+ }
+
+ private void onGenericRecognitionSuccessLocked(GenericRecognitionEvent event) {
+ if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) {
+ return;
+ }
+ ModelData model = getModelDataFor(event.soundModelHandle);
+ if (model == null) {
+ Slog.w(TAG, "Generic recognition event: Model does not exist for handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ IRecognitionStatusCallback callback = model.getCallback();
+ if (callback == null) {
+ Slog.w(TAG, "Generic recognition event: Null callback for model handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ try {
+ callback.onDetected((GenericRecognitionEvent) event);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onDetected", e);
+ }
+
+ model.setStopped();
+ RecognitionConfig config = model.getRecognitionConfig();
+ if (config == null) {
+ Slog.w(TAG, "Generic recognition event: Null RecognitionConfig for model handle: " +
+ event.soundModelHandle);
+ return;
+ }
+
+ // TODO: Remove this block if the lower layer supports multiple triggers.
+ if (config.allowMultipleTriggers) {
+ startGenericRecognitionLocked(model, true /* notify */);
+ }
+ }
+
@Override
public void onSoundModelUpdate(SoundModelEvent event) {
if (event == null) {
@@ -399,18 +617,25 @@
private void onRecognitionFailureLocked() {
Slog.w(TAG, "Recognition failure");
try {
- if (mActiveListener != null) {
- mActiveListener.onError(STATUS_ERROR);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onError(STATUS_ERROR);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
- internalClearStateLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGlobalStateLocked();
}
}
- private void onRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
+ private void onKeyphraseRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
Slog.i(TAG, "Recognition success");
+
+ if (mKeyphraseListener == null) {
+ Slog.w(TAG, "received onRecognition event without any listener for it");
+ return;
+ }
+
KeyphraseRecognitionExtra[] keyphraseExtras =
((KeyphraseRecognitionEvent) event).keyphraseExtras;
if (keyphraseExtras == null || keyphraseExtras.length == 0) {
@@ -424,14 +649,14 @@
}
try {
- if (mActiveListener != null) {
- mActiveListener.onDetected((KeyphraseRecognitionEvent) event);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onDetected((KeyphraseRecognitionEvent) event);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onDetected", e);
}
- mStarted = false;
+ mKeyphraseStarted = false;
mRequested = mRecognitionConfig.allowMultipleTriggers;
// TODO: Remove this block if the lower layer supports multiple triggers.
if (mRequested) {
@@ -441,14 +666,16 @@
private void onServiceDiedLocked() {
try {
- if (mActiveListener != null) {
- mActiveListener.onError(SoundTrigger.STATUS_DEAD_OBJECT);
+ if (mKeyphraseListener != null) {
+ mKeyphraseListener.onError(SoundTrigger.STATUS_DEAD_OBJECT);
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
} finally {
- internalClearSoundModelLocked();
- internalClearStateLocked();
+ internalClearKeyphraseSoundModelLocked();
+ internalClearKeyphraseStateLocked();
+ internalClearGenericModelStateLocked();
+ internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
mModule = null;
@@ -457,14 +684,14 @@
}
private int updateRecognitionLocked(boolean notify) {
- if (mModule == null || moduleProperties == null
- || mCurrentSoundModelHandle == INVALID_VALUE || mActiveListener == null) {
+ if (mModule == null || mModuleProperties == null
+ || mCurrentKeyphraseModelHandle == INVALID_VALUE || mKeyphraseListener == null) {
// Nothing to do here.
return STATUS_OK;
}
boolean start = mRequested && !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
- if (start == mStarted) {
+ if (start == mKeyphraseStarted) {
// No-op.
return STATUS_OK;
}
@@ -472,23 +699,24 @@
// See if the recognition needs to be started.
if (start) {
// Start recognition.
- int status = mModule.startRecognition(mCurrentSoundModelHandle, mRecognitionConfig);
+ int status = mModule.startRecognition(mCurrentKeyphraseModelHandle,
+ mRecognitionConfig);
if (status != SoundTrigger.STATUS_OK) {
Slog.w(TAG, "startRecognition failed with " + status);
// Notify of error if needed.
if (notify) {
try {
- mActiveListener.onError(status);
+ mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
- mStarted = true;
+ mKeyphraseStarted = true;
// Notify of resume if needed.
if (notify) {
try {
- mActiveListener.onRecognitionResumed();
+ mKeyphraseListener.onRecognitionResumed();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
}
@@ -499,7 +727,7 @@
// Stop recognition (only if we haven't been aborted).
int status = STATUS_OK;
if (!mRecognitionAborted) {
- status = mModule.stopRecognition(mCurrentSoundModelHandle);
+ status = mModule.stopRecognition(mCurrentKeyphraseModelHandle);
} else {
mRecognitionAborted = false;
}
@@ -507,17 +735,17 @@
Slog.w(TAG, "stopRecognition call failed with " + status);
if (notify) {
try {
- mActiveListener.onError(status);
+ mKeyphraseListener.onError(status);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onError", e);
}
}
} else {
- mStarted = false;
+ mKeyphraseStarted = false;
// Notify of pause if needed.
if (notify) {
try {
- mActiveListener.onRecognitionPaused();
+ mKeyphraseListener.onRecognitionPaused();
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
}
@@ -527,14 +755,11 @@
}
}
- private void internalClearStateLocked() {
- mStarted = false;
- mRequested = false;
-
- mKeyphraseId = INVALID_VALUE;
- mRecognitionConfig = null;
- mActiveListener = null;
-
+ // internalClearGlobalStateLocked() gets split into two routines. Cleanup that is
+ // specific to keyphrase sound models named as internalClearKeyphraseStateLocked() and
+ // internalClearGlobalStateLocked() for global state. The global cleanup routine will be used
+ // by the cleanup happening with the generic sound models.
+ private void internalClearGlobalStateLocked() {
// Unregister from call state changes.
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
@@ -545,8 +770,27 @@
}
}
- private void internalClearSoundModelLocked() {
- mCurrentSoundModelHandle = INVALID_VALUE;
+ private void internalClearKeyphraseStateLocked() {
+ mKeyphraseStarted = false;
+ mRequested = false;
+
+ mKeyphraseId = INVALID_VALUE;
+ mRecognitionConfig = null;
+ mKeyphraseListener = null;
+ }
+
+ private void internalClearGenericModelStateLocked() {
+ for (UUID modelId : mGenericModelDataMap.keySet()) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ modelData.clearState();
+ modelData.clearCallback();
+ }
+ }
+
+ // This routine is a replacement for internalClearSoundModelLocked(). However, we
+ // should see why this should be different from internalClearKeyphraseStateLocked().
+ private void internalClearKeyphraseSoundModelLocked() {
+ mCurrentKeyphraseModelHandle = INVALID_VALUE;
mCurrentSoundModel = null;
}
@@ -577,19 +821,251 @@
void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mLock) {
pw.print(" module properties=");
- pw.println(moduleProperties == null ? "null" : moduleProperties);
+ pw.println(mModuleProperties == null ? "null" : mModuleProperties);
pw.print(" keyphrase ID="); pw.println(mKeyphraseId);
- pw.print(" sound model handle="); pw.println(mCurrentSoundModelHandle);
+ pw.print(" sound model handle="); pw.println(mCurrentKeyphraseModelHandle);
pw.print(" sound model UUID=");
pw.println(mCurrentSoundModel == null ? "null" : mCurrentSoundModel.uuid);
pw.print(" current listener=");
- pw.println(mActiveListener == null ? "null" : mActiveListener.asBinder());
+ pw.println(mKeyphraseListener == null ? "null" : mKeyphraseListener.asBinder());
pw.print(" requested="); pw.println(mRequested);
- pw.print(" started="); pw.println(mStarted);
+ pw.print(" started="); pw.println(mKeyphraseStarted);
pw.print(" call active="); pw.println(mCallActive);
pw.print(" power save mode active="); pw.println(mIsPowerSaveMode);
pw.print(" service disabled="); pw.println(mServiceDisabled);
}
}
+
+ private void initializeTelephonyAndPowerStateListeners() {
+ // Get the current call state synchronously for the first recognition.
+ mCallActive = mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
+
+ // Register for call state changes when the first call to start recognition occurs.
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ // Register for power saver mode changes when the first call to start recognition
+ // occurs.
+ if (mPowerSaveModeListener == null) {
+ mPowerSaveModeListener = new PowerSaveModeListener();
+ mContext.registerReceiver(mPowerSaveModeListener,
+ new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
+ }
+ mIsPowerSaveMode = mPowerManager.isPowerSaveMode();
+ }
+
+ private ModelData getOrCreateGenericModelData(UUID modelId) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData == null) {
+ modelData = new ModelData(modelId);
+ modelData.setTypeGeneric();
+ mGenericModelDataMap.put(modelId, modelData);
+ }
+ return modelData;
+ }
+
+ // Instead of maintaining a second hashmap of modelHandle -> ModelData, we just
+ // iterate through to find the right object (since we don't expect 100s of models
+ // to be stored).
+ private ModelData getModelDataFor(int modelHandle) {
+ // Fetch ModelData object corresponding to the model handle.
+ for (ModelData model : mGenericModelDataMap.values()) {
+ if (model.getHandle() == modelHandle) {
+ return model;
+ }
+ }
+ return null;
+ }
+
+ // Whether we are allowed to run any recognition at all. The conditions that let us run
+ // a recognition include: no active phone call or not being in a power save mode. Also,
+ // the native service should be enabled.
+ private boolean isRecognitionAllowed() {
+ return !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
+ }
+
+ private int startGenericRecognitionLocked(ModelData modelData, boolean notify) {
+ IRecognitionStatusCallback callback = modelData.getCallback();
+ int handle = modelData.getHandle();
+ RecognitionConfig config = modelData.getRecognitionConfig();
+ if (callback == null || handle == INVALID_VALUE || config == null) {
+ // Nothing to do here.
+ Slog.w(TAG, "startGenericRecognition: Bad data passed in.");
+ return STATUS_ERROR;
+ }
+
+ if (!isRecognitionAllowed()) {
+ // Nothing to do here.
+ Slog.w(TAG, "startGenericRecognition requested but not allowed.");
+ return STATUS_OK;
+ }
+
+ int status = mModule.startRecognition(handle, config);
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "startRecognition failed with " + status);
+ // Notify of error if needed.
+ if (notify) {
+ try {
+ callback.onError(status);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onError", e);
+ }
+ }
+ } else {
+ modelData.setStarted();
+ // Notify of resume if needed.
+ if (notify) {
+ try {
+ callback.onRecognitionResumed();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
+ }
+ }
+ }
+ return status;
+ }
+
+ private int stopGenericRecognitionLocked(ModelData modelData, boolean notify) {
+ IRecognitionStatusCallback callback = modelData.getCallback();
+
+ // Stop recognition (only if we haven't been aborted).
+ int status = mModule.stopRecognition(modelData.getHandle());
+ if (status != SoundTrigger.STATUS_OK) {
+ Slog.w(TAG, "stopRecognition call failed with " + status);
+ if (notify) {
+ try {
+ callback.onError(status);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onError", e);
+ }
+ }
+ } else {
+ modelData.setStopped();
+ // Notify of pause if needed.
+ if (notify) {
+ try {
+ callback.onRecognitionPaused();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
+ }
+ }
+ }
+ return status;
+ }
+
+ // Computes whether we have any recognition running at all (voice or generic). Sets
+ // the mRecognitionRunning variable with the result.
+ private boolean computeRecognitionRunning() {
+ synchronized (mLock) {
+ if (mModuleProperties == null || mModule == null) {
+ mRecognitionRunning = false;
+ return mRecognitionRunning;
+ }
+ if (mKeyphraseListener != null &&
+ mKeyphraseStarted &&
+ mCurrentKeyphraseModelHandle != INVALID_VALUE &&
+ mCurrentSoundModel != null) {
+ mRecognitionRunning = true;
+ return mRecognitionRunning;
+ }
+ for (UUID modelId : mGenericModelDataMap.keySet()) {
+ ModelData modelData = mGenericModelDataMap.get(modelId);
+ if (modelData.modelStarted()) {
+ mRecognitionRunning = true;
+ return mRecognitionRunning;
+ }
+ }
+ mRecognitionRunning = false;
+ }
+ return mRecognitionRunning;
+ }
+
+ // This class encapsulates the callbacks, state, handles and any other information that
+ // represents a model.
+ private static class ModelData {
+ // Model not loaded (and hence not started).
+ static final int MODEL_NOTLOADED = 0;
+
+ // Loaded implies model was successfully loaded. Model not started yet.
+ static final int MODEL_LOADED = 1;
+
+ // Started implies model was successfully loaded and start was called.
+ static final int MODEL_STARTED = 2;
+
+ // One of MODEL_NOTLOADED, MODEL_LOADED, MODEL_STARTED (which implies loaded).
+ private int mModelState;
+
+ private UUID mModelId;
+
+ // One of SoundModel.TYPE_GENERIC or SoundModel.TYPE_KEYPHRASE. Initially set
+ // to SoundModel.TYPE_UNKNOWN;
+ private int mModelType = SoundModel.TYPE_UNKNOWN;
+ private IRecognitionStatusCallback mCallback = null;
+ private SoundModel mSoundModel = null;
+ private RecognitionConfig mRecognitionConfig = null;
+
+
+ // Model handle is an integer used by the HAL as an identifier for sound
+ // models.
+ private int mModelHandle = INVALID_VALUE;
+
+ ModelData(UUID modelId) {
+ mModelId = modelId;
+ }
+
+ synchronized void setTypeGeneric() {
+ mModelType = SoundModel.TYPE_GENERIC_SOUND;
+ }
+
+ synchronized void setCallback(IRecognitionStatusCallback callback) {
+ mCallback = callback;
+ }
+
+ synchronized IRecognitionStatusCallback getCallback() {
+ return mCallback;
+ }
+
+ synchronized boolean isModelLoaded() {
+ return (mModelState == MODEL_LOADED || mModelState == MODEL_STARTED) &&
+ mSoundModel != null;
+ }
+
+ synchronized void setStarted() {
+ mModelState = MODEL_STARTED;
+ }
+
+ synchronized void setStopped() {
+ mModelState = MODEL_LOADED;
+ }
+
+ synchronized boolean modelStarted() {
+ return mModelState == MODEL_STARTED;
+ }
+
+ synchronized void clearState() {
+ mModelState = MODEL_NOTLOADED;
+ mSoundModel = null;
+ mModelHandle = INVALID_VALUE;
+ }
+
+ synchronized void clearCallback() {
+ mCallback = null;
+ }
+
+ synchronized void setHandle(int handle) {
+ mModelHandle = handle;
+ }
+
+ synchronized void setRecognitionConfig(RecognitionConfig config) {
+ mRecognitionConfig = config;
+ }
+
+ synchronized int getHandle() {
+ return mModelHandle;
+ }
+
+ synchronized RecognitionConfig getRecognitionConfig() {
+ return mRecognitionConfig;
+ }
+ }
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 682f4a4..251f314 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -15,6 +15,7 @@
*/
package com.android.server.soundtrigger;
+import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -47,13 +48,14 @@
* @hide
*/
public class SoundTriggerService extends SystemService {
- static final String TAG = "SoundTriggerService";
- static final boolean DEBUG = false;
+ private static final String TAG = "SoundTriggerService";
+ private static final boolean DEBUG = true;
final Context mContext;
private final SoundTriggerServiceStub mServiceStub;
private final LocalSoundTriggerService mLocalSoundTriggerService;
private SoundTriggerDbHelper mDbHelper;
+ private SoundTriggerHelper mSoundTriggerHelper;
public SoundTriggerService(Context context) {
super(context);
@@ -71,7 +73,8 @@
@Override
public void onBootPhase(int phase) {
if (PHASE_SYSTEM_SERVICES_READY == phase) {
- mLocalSoundTriggerService.initSoundTriggerHelper();
+ initSoundTriggerHelper();
+ mLocalSoundTriggerService.setSoundTriggerHelper(mSoundTriggerHelper);
} else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
mDbHelper = new SoundTriggerDbHelper(mContext);
}
@@ -85,6 +88,20 @@
public void onSwitchUser(int userHandle) {
}
+ private synchronized void initSoundTriggerHelper() {
+ if (mSoundTriggerHelper == null) {
+ mSoundTriggerHelper = new SoundTriggerHelper(mContext);
+ }
+ }
+
+ private synchronized boolean isInitialized() {
+ if (mSoundTriggerHelper == null ) {
+ Slog.e(TAG, "SoundTriggerHelper not initialized.");
+ return false;
+ }
+ return true;
+ }
+
class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
@@ -102,19 +119,32 @@
}
@Override
- public void startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
+ public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
+ RecognitionConfig config) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
if (DEBUG) {
Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
}
+ if (!isInitialized()) return STATUS_ERROR;
+
+ GenericSoundModel model = getSoundModel(parcelUuid);
+ if (model == null) {
+ Slog.e(TAG, "Null model in database for id: " + parcelUuid);
+ return STATUS_ERROR;
+ }
+
+ return mSoundTriggerHelper.startGenericRecognition(parcelUuid.getUuid(), model,
+ callback, config);
}
@Override
- public void stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
+ public int stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
if (DEBUG) {
Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
}
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
}
@Override
@@ -123,10 +153,8 @@
if (DEBUG) {
Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
}
- SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(soundModelId.getUuid());
- if (model == null) {
- Slog.e(TAG, "Null model in database.");
- }
+ SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
+ soundModelId.getUuid());
return model;
}
@@ -157,38 +185,49 @@
mContext = context;
}
- void initSoundTriggerHelper() {
- if (mSoundTriggerHelper == null) {
- mSoundTriggerHelper = new SoundTriggerHelper(mContext);
- }
+ synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
+ mSoundTriggerHelper = helper;
}
@Override
public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
- return mSoundTriggerHelper.startRecognition(keyphraseId, soundModel, listener,
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel, listener,
recognitionConfig);
}
@Override
- public int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
- return mSoundTriggerHelper.stopRecognition(keyphraseId, listener);
+ public synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
+ if (!isInitialized()) return STATUS_ERROR;
+ return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
}
@Override
public void stopAllRecognitions() {
+ if (!isInitialized()) return;
mSoundTriggerHelper.stopAllRecognitions();
}
@Override
public ModuleProperties getModuleProperties() {
+ if (!isInitialized()) return null;
return mSoundTriggerHelper.getModuleProperties();
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!isInitialized()) return;
mSoundTriggerHelper.dump(fd, pw, args);
}
+
+ private synchronized boolean isInitialized() {
+ if (mSoundTriggerHelper == null ) {
+ Slog.e(TAG, "SoundTriggerHelper not initialized.");
+ return false;
+ }
+ return true;
+ }
}
private void enforceCallingPermission(String permission) {
diff --git a/tests/SoundTriggerTestApp/Android.mk b/tests/SoundTriggerTestApp/Android.mk
index 7bcab5e..c327b09 100644
--- a/tests/SoundTriggerTestApp/Android.mk
+++ b/tests/SoundTriggerTestApp/Android.mk
@@ -8,5 +8,6 @@
LOCAL_MODULE_TAGS := optional
LOCAL_PRIVILEGED_MODULE := true
+LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
diff --git a/tests/SoundTriggerTestApp/AndroidManifest.xml b/tests/SoundTriggerTestApp/AndroidManifest.xml
index 40619da..a72b3dd 100644
--- a/tests/SoundTriggerTestApp/AndroidManifest.xml
+++ b/tests/SoundTriggerTestApp/AndroidManifest.xml
@@ -2,16 +2,22 @@
package="com.android.test.soundtrigger">
<uses-permission android:name="android.permission.MANAGE_SOUND_TRIGGER" />
- <application
- android:permission="android.permission.MANAGE_SOUND_TRIGGER">
+ <application>
<activity
android:name="TestSoundTriggerActivity"
android:label="SoundTrigger Test Application"
- android:theme="@android:style/Theme.Material.Light.Voice">
+ android:theme="@android:style/Theme.Material">
+ <!--
<intent-filter>
<action android:name="com.android.intent.action.MANAGE_SOUND_TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
+ -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
</activity>
</application>
</manifest>
diff --git a/tests/SoundTriggerTestApp/res/layout/main.xml b/tests/SoundTriggerTestApp/res/layout/main.xml
index 9d2b9d9..5ecc770 100644
--- a/tests/SoundTriggerTestApp/res/layout/main.xml
+++ b/tests/SoundTriggerTestApp/res/layout/main.xml
@@ -18,6 +18,11 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:orientation="vertical"
+ >
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
>
<Button
@@ -37,7 +42,57 @@
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:text="@string/start_recog"
+ android:onClick="onStartRecognitionButtonClicked"
+ android:padding="20dp" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/stop_recog"
+ android:onClick="onStopRecognitionButtonClicked"
+ android:padding="20dp" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:text="@string/unenroll"
android:onClick="onUnEnrollButtonClicked"
android:padding="20dp" />
-</LinearLayout>
\ No newline at end of file
+
+</LinearLayout>
+
+<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="20dp"
+ android:orientation="vertical">
+ <RadioButton android:id="@+id/model_one"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_one"
+ android:onClick="onRadioButtonClicked"/>
+ <RadioButton android:id="@+id/model_two"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_two"
+ android:onClick="onRadioButtonClicked"/>
+ <RadioButton android:id="@+id/model_three"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/model_three"
+ android:onClick="onRadioButtonClicked"/>
+</RadioGroup>
+
+ <TextView
+ android:id="@+id/console"
+ android:gravity="left"
+ android:paddingTop="20pt"
+ android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:maxLines="40"
+ android:textSize="14dp"
+ android:scrollbars = "vertical"
+ android:text="@string/none">
+ </TextView>
+</LinearLayout>
diff --git a/tests/SoundTriggerTestApp/res/values/strings.xml b/tests/SoundTriggerTestApp/res/values/strings.xml
index 07bac2a..5f0fb1d 100644
--- a/tests/SoundTriggerTestApp/res/values/strings.xml
+++ b/tests/SoundTriggerTestApp/res/values/strings.xml
@@ -16,7 +16,13 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="enroll">Enroll</string>
- <string name="reenroll">Re-enroll</string>
- <string name="unenroll">Un-enroll</string>
-</resources>
\ No newline at end of file
+ <string name="enroll">Load</string>
+ <string name="reenroll">Re-load</string>
+ <string name="unenroll">Un-load</string>
+ <string name="start_recog">Start</string>
+ <string name="stop_recog">Stop</string>
+ <string name="model_one">Model One</string>
+ <string name="model_two">Model Two</string>
+ <string name="model_three">Model Three</string>
+ <string name="none">Debug messages appear here:</string>
+</resources>
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
index 4702835..1c95c25 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.media.soundtrigger.SoundTriggerDetector;
import android.media.soundtrigger.SoundTriggerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -28,6 +29,7 @@
import com.android.internal.app.ISoundTriggerService;
+import java.lang.RuntimeException;
import java.util.UUID;
/**
@@ -56,6 +58,9 @@
*/
public boolean addOrUpdateSoundModel(GenericSoundModel soundModel) {
try {
+ if (soundModel == null) {
+ throw new RuntimeException("Bad sound model");
+ }
mSoundTriggerService.updateSoundModel(soundModel);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in updateSoundModel", e);
@@ -112,4 +117,10 @@
public void deleteSoundModelUsingManager(UUID modelId) {
mSoundTriggerManager.deleteModel(modelId);
}
+
+ public SoundTriggerDetector createSoundTriggerDetector(UUID modelId,
+ SoundTriggerDetector.Callback callback) {
+ return mSoundTriggerManager.createSoundTriggerDetector(modelId, callback, null);
+ }
+
}
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
index 966179b..96a6966 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
@@ -22,11 +22,17 @@
import android.app.Activity;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.media.AudioFormat;
+import android.media.soundtrigger.SoundTriggerDetector;
import android.media.soundtrigger.SoundTriggerManager;
+import android.text.Editable;
+import android.text.method.ScrollingMovementMethod;
import android.os.Bundle;
import android.os.UserManager;
import android.util.Log;
import android.view.View;
+import android.widget.RadioButton;
+import android.widget.TextView;
import android.widget.Toast;
public class TestSoundTriggerActivity extends Activity {
@@ -35,42 +41,75 @@
private SoundTriggerUtil mSoundTriggerUtil;
private Random mRandom;
- private UUID mModelUuid = UUID.randomUUID();
+ private UUID mModelUuid1 = UUID.randomUUID();
private UUID mModelUuid2 = UUID.randomUUID();
+ private UUID mModelUuid3 = UUID.randomUUID();
private UUID mVendorUuid = UUID.randomUUID();
+ private SoundTriggerDetector mDetector1 = null;
+ private SoundTriggerDetector mDetector2 = null;
+ private SoundTriggerDetector mDetector3 = null;
+
+ private TextView mDebugView = null;
+ private int mSelectedModelId = 1;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DBG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
+ mDebugView = (TextView) findViewById(R.id.console);
+ mDebugView.setText(mDebugView.getText(), TextView.BufferType.EDITABLE);
+ mDebugView.setMovementMethod(new ScrollingMovementMethod());
mSoundTriggerUtil = new SoundTriggerUtil(this);
mRandom = new Random();
}
+ private void postMessage(String msg) {
+ Log.i(TAG, "Posted: " + msg);
+ ((Editable) mDebugView.getText()).append(msg + "\n");
+ }
+
+ private UUID getSelectedUuid() {
+ if (mSelectedModelId == 2) return mModelUuid2;
+ if (mSelectedModelId == 3) return mModelUuid3;
+ return mModelUuid1; // Default.
+ }
+
+ private void setDetector(SoundTriggerDetector detector) {
+ if (mSelectedModelId == 2) mDetector2 = detector;
+ if (mSelectedModelId == 3) mDetector3 = detector;
+ mDetector1 = detector;
+ }
+
+ private SoundTriggerDetector getDetector() {
+ if (mSelectedModelId == 2) return mDetector2;
+ if (mSelectedModelId == 3) return mDetector3;
+ return mDetector1;
+ }
+
/**
* Called when the user clicks the enroll button.
* Performs a fresh enrollment.
*/
public void onEnrollButtonClicked(View v) {
+ postMessage("Loading model: " + mSelectedModelId);
// Generate a fake model to push.
byte[] data = new byte[1024];
mRandom.nextBytes(data);
- GenericSoundModel model = new GenericSoundModel(mModelUuid, mVendorUuid, data);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel model = new GenericSoundModel(modelUuid, mVendorUuid, data);
boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(model);
if (status) {
Toast.makeText(
- this, "Successfully created sound trigger model UUID=" + mModelUuid, Toast.LENGTH_SHORT)
- .show();
+ this, "Successfully created sound trigger model UUID=" + modelUuid,
+ Toast.LENGTH_SHORT).show();
} else {
- Toast.makeText(this, "Failed to enroll!!!" + mModelUuid, Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, "Failed to enroll!!!" + modelUuid, Toast.LENGTH_SHORT).show();
}
// Test the SoundManager API.
- SoundTriggerManager.Model tmpModel = SoundTriggerManager.Model.create(mModelUuid2,
- mVendorUuid, data);
- mSoundTriggerUtil.addOrUpdateSoundModel(tmpModel);
}
/**
@@ -78,12 +117,14 @@
* Clears the enrollment information for the user.
*/
public void onUnEnrollButtonClicked(View v) {
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid);
+ postMessage("Unloading model: " + mSelectedModelId);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
return;
}
- boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid);
+ boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid1);
if (status) {
Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid,
Toast.LENGTH_SHORT)
@@ -91,7 +132,6 @@
} else {
Toast.makeText(this, "Failed to delete sound model!!!", Toast.LENGTH_SHORT).show();
}
- mSoundTriggerUtil.deleteSoundModelUsingManager(mModelUuid2);
}
/**
@@ -99,7 +139,9 @@
* Uses the previously enrolled sound model and makes changes to it before pushing it back.
*/
public void onReEnrollButtonClicked(View v) {
- GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid);
+ postMessage("Re-loading model: " + mSelectedModelId);
+ UUID modelUuid = getSelectedUuid();
+ GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
if (soundModel == null) {
Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
return;
@@ -118,4 +160,86 @@
Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show();
}
}
+
+ public void onStartRecognitionButtonClicked(View v) {
+ UUID modelUuid = getSelectedUuid();
+ SoundTriggerDetector detector = getDetector();
+ if (detector == null) {
+ Log.i(TAG, "Created an instance of the SoundTriggerDetector.");
+ detector = mSoundTriggerUtil.createSoundTriggerDetector(modelUuid,
+ new DetectorCallback());
+ setDetector(detector);
+ }
+ postMessage("Triggering start recognition for model: " + mSelectedModelId);
+ if (!detector.startRecognition(
+ SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
+ Log.e(TAG, "Fast failure attempting to start recognition.");
+ }
+ }
+
+ public void onStopRecognitionButtonClicked(View v) {
+ SoundTriggerDetector detector = getDetector();
+ if (detector == null) {
+ Log.e(TAG, "Stop called on null detector.");
+ return;
+ }
+ postMessage("Triggering stop recognition for model: " + mSelectedModelId);
+ if (!detector.stopRecognition()) {
+ Log.e(TAG, "Fast failure attempting to stop recognition.");
+ }
+ }
+
+ public void onRadioButtonClicked(View view) {
+ // Is the button now checked?
+ boolean checked = ((RadioButton) view).isChecked();
+ // Check which radio button was clicked
+ switch(view.getId()) {
+ case R.id.model_one:
+ if (checked) mSelectedModelId = 1;
+ postMessage("Selected model one.");
+ break;
+ case R.id.model_two:
+ if (checked) mSelectedModelId = 2;
+ postMessage("Selected model two.");
+ break;
+ case R.id.model_three:
+ if (checked) mSelectedModelId = 3;
+ postMessage("Selected model three.");
+ break;
+ }
+ }
+
+ // Implementation of SoundTriggerDetector.Callback.
+ public class DetectorCallback extends SoundTriggerDetector.Callback {
+ public void onAvailabilityChanged(int status) {
+ postMessage("Availability changed to: " + status);
+ }
+
+ public void onDetected(SoundTriggerDetector.EventPayload event) {
+ postMessage("onDetected(): " + eventPayloadToString(event));
+ }
+
+ public void onError() {
+ postMessage("onError()");
+ }
+
+ public void onRecognitionPaused() {
+ postMessage("onRecognitionPaused()");
+ }
+
+ public void onRecognitionResumed() {
+ postMessage("onRecognitionResumed()");
+ }
+ }
+
+ private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
+ String result = "EventPayload(";
+ AudioFormat format = event.getCaptureAudioFormat();
+ result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
+ byte[] triggerAudio = event.getTriggerAudio();
+ result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length);
+ result = result + "CaptureSession: " + event.getCaptureSession();
+ result += " )";
+ return result;
+ }
}