diff options
153 files changed, 8105 insertions, 1967 deletions
diff --git a/Android.mk b/Android.mk index ffc04217f6cd..08f34688fb51 100644 --- a/Android.mk +++ b/Android.mk @@ -149,6 +149,7 @@ LOCAL_SRC_FILES += \ core/java/android/content/pm/IPackageMoveObserver.aidl \ core/java/android/content/pm/IPackageStatsObserver.aidl \ core/java/android/content/pm/IOnPermissionsChangeListener.aidl \ + core/java/android/content/pm/IShortcutService.aidl \ core/java/android/database/IContentObserver.aidl \ ../av/camera/aidl/android/hardware/ICameraService.aidl \ ../av/camera/aidl/android/hardware/ICameraServiceListener.aidl \ @@ -652,6 +653,7 @@ aidl_files := \ frameworks/base/core/java/android/content/pm/ProviderInfo.aidl \ frameworks/base/core/java/android/content/pm/PackageStats.aidl \ frameworks/base/core/java/android/content/pm/PermissionGroupInfo.aidl \ + frameworks/base/core/java/android/content/pm/ShortcutInfo.aidl \ frameworks/base/core/java/android/content/pm/LabeledIntent.aidl \ frameworks/base/core/java/android/content/ComponentName.aidl \ frameworks/base/core/java/android/content/SyncStats.aidl \ diff --git a/api/current.txt b/api/current.txt index 5ec93cbd57e1..cc386db74ef4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5885,7 +5885,7 @@ package android.app.admin { method public android.app.admin.SystemUpdatePolicy getSystemUpdatePolicy(); method public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName); method public android.os.Bundle getUserRestrictions(android.content.ComponentName); - method public java.lang.String getWifiMacAddress(); + method public java.lang.String getWifiMacAddress(android.content.ComponentName); method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]); method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean installCaCert(android.content.ComponentName, byte[]); @@ -8140,6 +8140,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9445,13 +9446,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcutInfo(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public boolean startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9464,6 +9471,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -9963,6 +9982,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR; + field public static final int FLAG_DYNAMIC = 1; // 0x1 + field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 + field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -23023,6 +23092,7 @@ package android.media.tv { method public boolean onTouchEvent(android.view.MotionEvent); method public boolean onTrackballEvent(android.view.MotionEvent); method public abstract boolean onTune(android.net.Uri); + method public boolean onTune(android.net.Uri, android.os.Bundle); method public void onUnblockContent(android.media.tv.TvContentRating); method public void setOverlayViewEnabled(boolean); } @@ -23096,12 +23166,15 @@ package android.media.tv { method public void setOnUnhandledInputEventListener(android.media.tv.TvView.OnUnhandledInputEventListener); method public void setStreamVolume(float); method public void setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback); + method public void setZOrderMediaOverlay(boolean); + method public void setZOrderOnTop(boolean); method public void timeShiftPause(); method public void timeShiftPlay(java.lang.String, android.net.Uri); method public void timeShiftResume(); method public void timeShiftSeekTo(long); method public void timeShiftSetPlaybackParams(android.media.PlaybackParams); method public void tune(java.lang.String, android.net.Uri); + method public void tune(java.lang.String, android.net.Uri, android.os.Bundle); } public static abstract interface TvView.OnUnhandledInputEventListener { @@ -29007,6 +29080,7 @@ package android.os { ctor public PersistableBundle(); ctor public PersistableBundle(int); ctor public PersistableBundle(android.os.PersistableBundle); + ctor public PersistableBundle(android.os.Bundle); method public java.lang.Object clone(); method public int describeContents(); method public android.os.PersistableBundle getPersistableBundle(java.lang.String); @@ -34426,8 +34500,9 @@ package android.service.media { package android.service.notification { public class Condition implements android.os.Parcelable { - ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int); + ctor public Condition(android.net.Uri, java.lang.String, int); ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int, int, int); + ctor public Condition(android.os.Parcel); method public android.service.notification.Condition copy(); method public int describeContents(); method public static boolean isValidId(android.net.Uri, java.lang.String); @@ -34458,6 +34533,7 @@ package android.service.notification { method public final void notifyConditions(android.service.notification.Condition...); method public android.os.IBinder onBind(android.content.Intent); method public abstract void onConnected(); + method public void onRequestConditions(int); method public abstract void onSubscribe(android.net.Uri); method public abstract void onUnsubscribe(android.net.Uri); field public static final java.lang.String EXTRA_RULE_ID = "android.content.automatic.ruleId"; @@ -57404,7 +57480,23 @@ package java.util { public abstract interface Comparator { method public abstract int compare(T, T); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>); + method public static java.util.Comparator<T> comparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public static java.util.Comparator<T> comparingInt(java.util.function.ToIntFunction<? super T>); + method public static java.util.Comparator<T> comparingLong(java.util.function.ToLongFunction<? super T>); method public abstract boolean equals(java.lang.Object); + method public static java.util.Comparator<T> naturalOrder(); + method public static java.util.Comparator<T> nullsFirst(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> nullsLast(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> reverseOrder(); + method public default java.util.Comparator<T> reversed(); + method public default java.util.Comparator<T> thenComparing(java.util.Comparator<? super T>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>); + method public default java.util.Comparator<T> thenComparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public default java.util.Comparator<T> thenComparingInt(java.util.function.ToIntFunction<? super T>); + method public default java.util.Comparator<T> thenComparingLong(java.util.function.ToLongFunction<? super T>); } public class ConcurrentModificationException extends java.lang.RuntimeException { diff --git a/api/removed.txt b/api/removed.txt index 115224ce49a6..2f553734068a 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -43,6 +43,14 @@ package android.media { } +package android.media.tv { + + public class TvView extends android.view.ViewGroup { + method public void requestUnblockContent(android.media.tv.TvContentRating); + } + +} + package android.net { public class SSLCertificateSocketFactory extends javax.net.ssl.SSLSocketFactory { @@ -201,14 +209,6 @@ package android.provider { } -package android.service.notification { - - public abstract class ConditionProviderService extends android.app.Service { - method public void onRequestConditions(int); - } - -} - package android.test.mock { public deprecated class MockPackageManager extends android.content.pm.PackageManager { diff --git a/api/system-current.txt b/api/system-current.txt index 4224792ccaf1..7823b3cd16ba 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6031,7 +6031,7 @@ package android.app.admin { method public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName); method public int getUserProvisioningState(); method public android.os.Bundle getUserRestrictions(android.content.ComponentName); - method public java.lang.String getWifiMacAddress(); + method public java.lang.String getWifiMacAddress(android.content.ComponentName); method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]); method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean installCaCert(android.content.ComponentName, byte[]); @@ -8446,6 +8446,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9779,13 +9780,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcutInfo(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public boolean startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9798,6 +9805,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -10357,6 +10376,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR; + field public static final int FLAG_DYNAMIC = 1; // 0x1 + field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 + field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -24872,7 +24941,6 @@ package android.media.tv { method public java.util.List<android.media.tv.TvTrackInfo> getTracks(int); method protected void onLayout(boolean, int, int, int, int); method public boolean onUnhandledInputEvent(android.view.InputEvent); - method public deprecated void requestUnblockContent(android.media.tv.TvContentRating); method public void reset(); method public void selectTrack(int, java.lang.String); method public void sendAppPrivateCommand(java.lang.String, android.os.Bundle); @@ -31292,6 +31360,7 @@ package android.os { ctor public PersistableBundle(); ctor public PersistableBundle(int); ctor public PersistableBundle(android.os.PersistableBundle); + ctor public PersistableBundle(android.os.Bundle); method public java.lang.Object clone(); method public int describeContents(); method public android.os.PersistableBundle getPersistableBundle(java.lang.String); @@ -36916,8 +36985,9 @@ package android.service.media { package android.service.notification { public class Condition implements android.os.Parcelable { - ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int); + ctor public Condition(android.net.Uri, java.lang.String, int); ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int, int, int); + ctor public Condition(android.os.Parcel); method public android.service.notification.Condition copy(); method public int describeContents(); method public static boolean isValidId(android.net.Uri, java.lang.String); @@ -60508,7 +60578,23 @@ package java.util { public abstract interface Comparator { method public abstract int compare(T, T); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>); + method public static java.util.Comparator<T> comparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public static java.util.Comparator<T> comparingInt(java.util.function.ToIntFunction<? super T>); + method public static java.util.Comparator<T> comparingLong(java.util.function.ToLongFunction<? super T>); method public abstract boolean equals(java.lang.Object); + method public static java.util.Comparator<T> naturalOrder(); + method public static java.util.Comparator<T> nullsFirst(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> nullsLast(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> reverseOrder(); + method public default java.util.Comparator<T> reversed(); + method public default java.util.Comparator<T> thenComparing(java.util.Comparator<? super T>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>); + method public default java.util.Comparator<T> thenComparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public default java.util.Comparator<T> thenComparingInt(java.util.function.ToIntFunction<? super T>); + method public default java.util.Comparator<T> thenComparingLong(java.util.function.ToLongFunction<? super T>); } public class ConcurrentModificationException extends java.lang.RuntimeException { diff --git a/api/system-removed.txt b/api/system-removed.txt index 9ebc484e6dd7..79f72975a194 100644 --- a/api/system-removed.txt +++ b/api/system-removed.txt @@ -41,6 +41,14 @@ package android.media { } +package android.media.tv { + + public class TvView extends android.view.ViewGroup { + method public void requestUnblockContent(android.media.tv.TvContentRating); + } + +} + package android.net { public class SSLCertificateSocketFactory extends javax.net.ssl.SSLSocketFactory { diff --git a/api/test-current.txt b/api/test-current.txt index 894971b2d2b7..a478793a70bf 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5889,7 +5889,7 @@ package android.app.admin { method public android.app.admin.SystemUpdatePolicy getSystemUpdatePolicy(); method public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName); method public android.os.Bundle getUserRestrictions(android.content.ComponentName); - method public java.lang.String getWifiMacAddress(); + method public java.lang.String getWifiMacAddress(android.content.ComponentName); method public boolean hasCaCertInstalled(android.content.ComponentName, byte[]); method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean installCaCert(android.content.ComponentName, byte[]); @@ -8146,6 +8146,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9454,13 +9455,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcutInfo(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); + method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List<java.lang.String>, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public boolean startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9473,6 +9480,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -9973,6 +9992,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR; + field public static final int FLAG_DYNAMIC = 1; // 0x1 + field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 + field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -23034,6 +23103,7 @@ package android.media.tv { method public boolean onTouchEvent(android.view.MotionEvent); method public boolean onTrackballEvent(android.view.MotionEvent); method public abstract boolean onTune(android.net.Uri); + method public boolean onTune(android.net.Uri, android.os.Bundle); method public void onUnblockContent(android.media.tv.TvContentRating); method public void setOverlayViewEnabled(boolean); } @@ -23107,12 +23177,15 @@ package android.media.tv { method public void setOnUnhandledInputEventListener(android.media.tv.TvView.OnUnhandledInputEventListener); method public void setStreamVolume(float); method public void setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback); + method public void setZOrderMediaOverlay(boolean); + method public void setZOrderOnTop(boolean); method public void timeShiftPause(); method public void timeShiftPlay(java.lang.String, android.net.Uri); method public void timeShiftResume(); method public void timeShiftSeekTo(long); method public void timeShiftSetPlaybackParams(android.media.PlaybackParams); method public void tune(java.lang.String, android.net.Uri); + method public void tune(java.lang.String, android.net.Uri, android.os.Bundle); } public static abstract interface TvView.OnUnhandledInputEventListener { @@ -29018,6 +29091,7 @@ package android.os { ctor public PersistableBundle(); ctor public PersistableBundle(int); ctor public PersistableBundle(android.os.PersistableBundle); + ctor public PersistableBundle(android.os.Bundle); method public java.lang.Object clone(); method public int describeContents(); method public android.os.PersistableBundle getPersistableBundle(java.lang.String); @@ -34443,8 +34517,9 @@ package android.service.media { package android.service.notification { public class Condition implements android.os.Parcelable { - ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int); + ctor public Condition(android.net.Uri, java.lang.String, int); ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int, int, int); + ctor public Condition(android.os.Parcel); method public android.service.notification.Condition copy(); method public int describeContents(); method public static boolean isValidId(android.net.Uri, java.lang.String); @@ -34475,6 +34550,7 @@ package android.service.notification { method public final void notifyConditions(android.service.notification.Condition...); method public android.os.IBinder onBind(android.content.Intent); method public abstract void onConnected(); + method public void onRequestConditions(int); method public abstract void onSubscribe(android.net.Uri); method public abstract void onUnsubscribe(android.net.Uri); field public static final java.lang.String EXTRA_RULE_ID = "android.content.automatic.ruleId"; @@ -57423,7 +57499,23 @@ package java.util { public abstract interface Comparator { method public abstract int compare(T, T); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public static java.util.Comparator<T> comparing(java.util.function.Function<? super T, ? extends U>); + method public static java.util.Comparator<T> comparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public static java.util.Comparator<T> comparingInt(java.util.function.ToIntFunction<? super T>); + method public static java.util.Comparator<T> comparingLong(java.util.function.ToLongFunction<? super T>); method public abstract boolean equals(java.lang.Object); + method public static java.util.Comparator<T> naturalOrder(); + method public static java.util.Comparator<T> nullsFirst(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> nullsLast(java.util.Comparator<? super T>); + method public static java.util.Comparator<T> reverseOrder(); + method public default java.util.Comparator<T> reversed(); + method public default java.util.Comparator<T> thenComparing(java.util.Comparator<? super T>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>, java.util.Comparator<? super U>); + method public default java.util.Comparator<T> thenComparing(java.util.function.Function<? super T, ? extends U>); + method public default java.util.Comparator<T> thenComparingDouble(java.util.function.ToDoubleFunction<? super T>); + method public default java.util.Comparator<T> thenComparingInt(java.util.function.ToIntFunction<? super T>); + method public default java.util.Comparator<T> thenComparingLong(java.util.function.ToLongFunction<? super T>); } public class ConcurrentModificationException extends java.lang.RuntimeException { diff --git a/api/test-removed.txt b/api/test-removed.txt index 115224ce49a6..2f553734068a 100644 --- a/api/test-removed.txt +++ b/api/test-removed.txt @@ -43,6 +43,14 @@ package android.media { } +package android.media.tv { + + public class TvView extends android.view.ViewGroup { + method public void requestUnblockContent(android.media.tv.TvContentRating); + } + +} + package android.net { public class SSLCertificateSocketFactory extends javax.net.ssl.SSLSocketFactory { @@ -201,14 +209,6 @@ package android.provider { } -package android.service.notification { - - public abstract class ConditionProviderService extends android.app.Service { - method public void onRequestConditions(int); - } - -} - package android.test.mock { public deprecated class MockPackageManager extends android.content.pm.PackageManager { diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 1f1f318be5c1..cd4ace669b6c 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -42,7 +42,7 @@ public final class AutomaticZenRule implements Parcelable { * @param name The name of the rule. * @param owner The Condition Provider service that owns this rule. * @param conditionId A representation of the state that should cause the Condition Provider - * service to apply the interruption filter. + * service to apply the given interruption filter. * @param interruptionFilter The interruption filter defines which notifications are allowed to * interrupt the user (e.g. via sound & vibration) while this rule * is active. diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index d8f0ac518151..a2cc6b17c376 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -47,6 +47,7 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; +import android.text.style.CharacterStyle; import android.text.style.RelativeSizeSpan; import android.text.style.TextAppearanceSpan; import android.util.Log; @@ -1664,17 +1665,22 @@ public class Notification implements Parcelable SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); for (Object span : spans) { Object resultSpan = span; - if (span instanceof TextAppearanceSpan) { - TextAppearanceSpan originalSpan = (TextAppearanceSpan) span; + if (resultSpan instanceof CharacterStyle) { + resultSpan = ((CharacterStyle) span).getUnderlying(); + } + if (resultSpan instanceof TextAppearanceSpan) { + TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; resultSpan = new TextAppearanceSpan( originalSpan.getFamily(), originalSpan.getTextStyle(), -1, originalSpan.getTextColor(), originalSpan.getLinkTextColor()); - } else if (span instanceof RelativeSizeSpan - || span instanceof AbsoluteSizeSpan) { + } else if (resultSpan instanceof RelativeSizeSpan + || resultSpan instanceof AbsoluteSizeSpan) { continue; + } else { + resultSpan = span; } builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), ss.getSpanFlags(span)); @@ -3591,37 +3597,53 @@ public class Notification implements Parcelable } /** + * Removes RemoteViews that were created for compatibility from {@param n}, if they did not + * change. + * + * @return {@param n}, if no stripping is needed, otherwise a stripped clone of {@param n}. + * * @hide */ - public static void stripForDelivery(Notification n) { + public static Notification maybeCloneStrippedForDelivery(Notification n) { String templateClass = n.extras.getString(EXTRA_TEMPLATE); - if (TextUtils.isEmpty(templateClass)) { - return; - } + // Only strip views for known Styles because we won't know how to // re-create them otherwise. - if (getNotificationStyleClass(templateClass) == null) { - return; + if (!TextUtils.isEmpty(templateClass) + && getNotificationStyleClass(templateClass) == null) { + return n; } - // Get rid of unmodified BuilderRemoteViews. - if (n.contentView instanceof BuilderRemoteViews && + + // Only strip unmodified BuilderRemoteViews. + boolean stripContentView = n.contentView instanceof BuilderRemoteViews && n.extras.getInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT, -1) == - n.contentView.getSequenceNumber()) { - n.contentView = null; - n.extras.remove(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT); - } - if (n.bigContentView instanceof BuilderRemoteViews && + n.contentView.getSequenceNumber(); + boolean stripBigContentView = n.bigContentView instanceof BuilderRemoteViews && n.extras.getInt(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT, -1) == - n.bigContentView.getSequenceNumber()) { - n.bigContentView = null; - n.extras.remove(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT); - } - if (n.headsUpContentView instanceof BuilderRemoteViews && + n.bigContentView.getSequenceNumber(); + boolean stripHeadsUpContentView = n.headsUpContentView instanceof BuilderRemoteViews && n.extras.getInt(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT, -1) == - n.headsUpContentView.getSequenceNumber()) { - n.headsUpContentView = null; - n.extras.remove(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT); + n.headsUpContentView.getSequenceNumber(); + + // Nothing to do here, no need to clone. + if (!stripContentView && !stripBigContentView && !stripHeadsUpContentView) { + return n; + } + + Notification clone = n.clone(); + if (stripContentView) { + clone.contentView = null; + clone.extras.remove(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT); + } + if (stripBigContentView) { + clone.bigContentView = null; + clone.extras.remove(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT); + } + if (stripHeadsUpContentView) { + clone.headsUpContentView = null; + clone.extras.remove(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT); } + return clone; } private int getBaseLayoutResource() { diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 16aee786d80c..7cfa0cd36102 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -259,8 +259,7 @@ public class NotificationManager } } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); - final Notification copy = notification.clone(); - Builder.stripForDelivery(copy); + final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, idOut, user.getIdentifier()); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index b1c5fd8c6963..3a5dd30bfda7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -37,7 +37,9 @@ import android.content.Context; import android.content.IRestrictionsManager; import android.content.RestrictionsManager; import android.content.pm.ILauncherApps; +import android.content.pm.IShortcutService; import android.content.pm.LauncherApps; +import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; @@ -748,6 +750,15 @@ final class SystemServiceRegistry { Log.i(TAG, "Creating new instance of SoundTriggerManager object."); return new SoundTriggerManager(ctx, ISoundTriggerService.Stub.asInterface(b)); }}); + + registerService(Context.SHORTCUT_SERVICE, ShortcutManager.class, + new CachedServiceFetcher<ShortcutManager>() { + @Override + public ShortcutManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.SHORTCUT_SERVICE); + return new ShortcutManager(ctx, + IShortcutService.Stub.asInterface(b)); + }}); } /** diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c4037f867f5a..8bd40dc66491 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -5542,14 +5542,15 @@ public class DevicePolicyManager { /** * Called by device owner to get the MAC address of the Wi-Fi device. * + * @param admin Which device owner this request is associated with. * @return the MAC address of the Wi-Fi device, or null when the information is not * available. (For example, Wi-Fi hasn't been enabled, or the device doesn't support Wi-Fi.) * * <p>The address will be in the {@code XX:XX:XX:XX:XX:XX} format. */ - public String getWifiMacAddress() { + public String getWifiMacAddress(@NonNull ComponentName admin) { try { - return mService.getWifiMacAddress(); + return mService.getWifiMacAddress(admin); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index b7a16aa0dc07..dc73e2613f18 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -262,7 +262,7 @@ interface IDevicePolicyManager { List<String> getKeepUninstalledPackages(in ComponentName admin); boolean isManagedProfile(in ComponentName admin); boolean isSystemOnlyUser(in ComponentName admin); - String getWifiMacAddress(); + String getWifiMacAddress(in ComponentName admin); void reboot(in ComponentName admin); void setShortSupportMessage(in ComponentName admin, in String message); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b935b256b880..f96ddf0d183a 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2680,6 +2680,7 @@ public abstract class Context { RADIO_SERVICE, HARDWARE_PROPERTIES_SERVICE, //@hide: SOUND_TRIGGER_SERVICE, + SHORTCUT_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -3576,6 +3577,14 @@ public abstract class Context { public static final String HARDWARE_PROPERTIES_SERVICE = "hardwareproperties"; /** + * TODO Javadoc + * + * @see #getSystemService + * @see android.content.pm.ShortcutManager + */ + public static final String SHORTCUT_SERVICE = "shortcut"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index cc266c552760..cf3e298fd6d7 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -22,6 +22,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IOnAppsChangedListener; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.UserHandle; @@ -42,4 +43,13 @@ interface ILauncherApps { boolean isPackageEnabled(String packageName, in UserHandle user); boolean isActivityEnabled(in ComponentName component, in UserHandle user); ApplicationInfo getApplicationInfo(String packageName, int flags, in UserHandle user); + + ParceledListSlice getShortcuts(String callingPackage, long changedSince, String packageName, + in ComponentName componentName, int flags, in UserHandle user); + ParceledListSlice getShortcutInfo(String callingPackage, String packageName, in List<String> ids, + in UserHandle user); + void pinShortcuts(String callingPackage, String packageName, in List<String> shortcutIds, + in UserHandle user); + boolean startShortcut(String callingPackage, String packageName, String id, + in Rect sourceBounds, in Bundle startActivityOptions, in UserHandle user); } diff --git a/core/java/android/content/pm/IOnAppsChangedListener.aidl b/core/java/android/content/pm/IOnAppsChangedListener.aidl index 130369661e05..e6525af311ed 100644 --- a/core/java/android/content/pm/IOnAppsChangedListener.aidl +++ b/core/java/android/content/pm/IOnAppsChangedListener.aidl @@ -16,6 +16,7 @@ package android.content.pm; +import android.content.pm.ParceledListSlice; import android.os.UserHandle; /** @@ -29,4 +30,5 @@ oneway interface IOnAppsChangedListener { void onPackagesUnavailable(in UserHandle user, in String[] packageNames, boolean replacing); void onPackagesSuspended(in UserHandle user, in String[] packageNames); void onPackagesUnsuspended(in UserHandle user, in String[] packageNames); + void onShortcutChanged(in UserHandle user, String packageName, in ParceledListSlice shortcuts); } diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl new file mode 100644 index 000000000000..23e671d82f12 --- /dev/null +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.content.pm.ParceledListSlice; +import android.content.pm.ShortcutInfo; + +/** + * {@hide} + */ +interface IShortcutService { + + boolean setDynamicShortcuts(String packageName, in ParceledListSlice shortcutInfoList, + int userId); + + ParceledListSlice getDynamicShortcuts(String packageName, int userId); + + boolean addDynamicShortcut(String packageName, in ShortcutInfo shortcutInfo, int userId); + + void deleteDynamicShortcut(String packageName, in String shortcutId, int userId); + + void deleteAllDynamicShortcuts(String packageName, int userId); + + ParceledListSlice getPinnedShortcuts(String packageName, int userId); + + boolean updateShortcuts(String packageName, in ParceledListSlice shortcuts, int userId); + + int getMaxDynamicShortcutCount(String packageName, int userId); + + int getRemainingCallCount(String packageName, int userId); + + long getRateLimitResetTime(String packageName, int userId); + + void resetThrottling(); // system only API for developer opsions +}
\ No newline at end of file diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index e443d5073c54..8e4a0e20097e 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -16,23 +16,29 @@ package android.content.pm; +import android.Manifest.permission; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ILauncherApps; -import android.content.pm.IOnAppsChangedListener; import android.content.pm.PackageManager.ApplicationInfoFlags; -import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -148,6 +154,91 @@ public class LauncherApps { */ public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { } + + /** + * Indicates that one or more shortcuts (which may be dynamic and/or pinned) + * have been added, updated or removed. + * + * @param packageName The name of the package that has the shortcuts. + * @param shortcuts all shortcuts from the package (dynamic and/or pinned). + * @param user The UserHandle of the profile that generated the change. + */ + public void onShortcutsChanged(@NonNull String packageName, + @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { + } + } + + /** + * Represents a query passed to {@link #getShortcuts(ShortcutQuery, UserHandle)}. + */ + public static class ShortcutQuery { + /** + * Include dynamic shortcuts in the result. + */ + public static final int FLAG_GET_DYNAMIC = 1 << 0; + + /** + * Include pinned shortcuts in the result. + */ + public static final int FLAG_GET_PINNED = 1 << 1; + + /** + * Requests "key" fields only. + */ + public static final int FLAG_GET_KEY_FIELDS_ONLY = 1 << 2; + + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_GET_DYNAMIC, + FLAG_GET_PINNED, + FLAG_GET_KEY_FIELDS_ONLY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface QueryFlags {} + + long mChangedSince; + + @Nullable + String mPackage; + + @Nullable + ComponentName mActivity; + + @QueryFlags + int mQueryFlags; + + public ShortcutQuery() { + } + + /** + * If non-zero, returns only shortcuts that have been added or updated since the timestamp, + * which is a milliseconds since the Epoch. + */ + public void setChangedSince(long changedSince) { + mChangedSince = changedSince; + } + + /** + * If non-null, returns only shortcuts from the package. + */ + public void setPackage(@Nullable String packageName) { + mPackage = packageName; + } + + /** + * If non-null, returns only shortcuts associated with the activity. + */ + public void setActivity(@Nullable ComponentName activity) { + mActivity = activity; + } + + /** + * Set query options. + */ + public void setQueryFlags(@QueryFlags int queryFlags) { + mQueryFlags = queryFlags; + } } /** @hide */ @@ -302,6 +393,128 @@ public class LauncherApps { } } + /** + * Returns the IDs of {@link ShortcutInfo}s that match {@code query}. + * + * <p>Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param query result includes shortcuts matching this query. + * @param user The UserHandle of the profile. + * + * @return the IDs of {@link ShortcutInfo}s that match the query. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + @Nullable + public List<ShortcutInfo> getShortcuts(@NonNull ShortcutQuery query, + @NonNull UserHandle user) { + try { + return mService.getShortcuts(mContext.getPackageName(), + query.mChangedSince, query.mPackage, query.mActivity, query.mQueryFlags, user) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@link ShortcutInfo}s with the given IDs from a package. + * + * <p>Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param packageName The target package. + * @param ids IDs of the shortcuts to retrieve. + * @param user The UserHandle of the profile. + * + * @return list of {@link ShortcutInfo} associated with the package. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + @Nullable + public List<ShortcutInfo> getShortcutInfo(@NonNull String packageName, + @NonNull List<String> ids, @NonNull UserHandle user) { + try { + return mService.getShortcutInfo(mContext.getPackageName(), packageName, ids, user) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Pin shortcuts on a package. + * + * <p>This API is <b>NOT</b> cumulative; this will replace all pinned shortcuts for the package. + * However, different launchers may have different set of pinned shortcuts. + * + * <p>Callers must have the {@link permission#BIND_APPWIDGET} permission. + * + * @param packageName The target package name. + * @param shortcutIds The IDs of the shortcut to be pinned. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public void pinShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds, + @NonNull UserHandle user) { + try { + mService.pinShortcuts(mContext.getPackageName(), packageName, shortcutIds, user); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the icon resource ID, if {@code shortcut} has one + * (i.e. when {@link ShortcutInfo#hasIconResource()} returns {@code true}). + * + * <p>Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param shortcut The target shortcut. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public int getShortcutIconResId(@NonNull ShortcutInfo shortcut, @NonNull UserHandle user) { + throw new RuntimeException("not implemented yet"); + } + + /** + * Return the icon as {@link ParcelFileDescriptor}, when it's stored as a file + * (i.e. when {@link ShortcutInfo#hasIconFile()} returns {@code true}). + * + * <p>Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param shortcut The target shortcut. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public ParcelFileDescriptor getShortcutIconFd( + @NonNull ShortcutInfo shortcut, @NonNull UserHandle user) { + throw new RuntimeException("not implemented yet"); + } + + /** + * Launches a shortcut. + * + * <p>Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param packageName The target shortcut package name. + * @param shortcutId The target shortcut ID. + * @param sourceBounds The Rect containing the source bounds of the clicked icon. + * @param startActivityOptions Options to pass to startActivity. + * @param user The UserHandle of the profile. + * @return {@code false} when the shortcut is no longer valid (e.g. the creator application + * has been uninstalled). {@code true} when the shortcut is still valid. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public boolean startShortcut(@NonNull String packageName, @NonNull String shortcutId, + @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions, + @NonNull UserHandle user) { + try { + return mService.startShortcut(mContext.getPackageName(), packageName, shortcutId, + sourceBounds, startActivityOptions, user); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Registers a callback for changes to packages in current and managed profiles. @@ -474,6 +687,20 @@ public class LauncherApps { } } } + + @Override + public void onShortcutChanged(UserHandle user, String packageName, + ParceledListSlice shortcuts) { + if (DEBUG) { + Log.d(TAG, "onShortcutChanged " + user.getIdentifier() + "," + packageName); + } + final List<ShortcutInfo> list = shortcuts.getList(); + synchronized (LauncherApps.this) { + for (CallbackMessageHandler callback : mCallbacks) { + callback.postOnShortcutChanged(packageName, user, list); + } + } + } }; private static class CallbackMessageHandler extends Handler { @@ -484,6 +711,7 @@ public class LauncherApps { private static final int MSG_UNAVAILABLE = 5; private static final int MSG_SUSPENDED = 6; private static final int MSG_UNSUSPENDED = 7; + private static final int MSG_SHORTCUT_CHANGED = 8; private LauncherApps.Callback mCallback; @@ -492,6 +720,7 @@ public class LauncherApps { String packageName; boolean replacing; UserHandle user; + List<ShortcutInfo> shortcuts; } public CallbackMessageHandler(Looper looper, LauncherApps.Callback callback) { @@ -527,6 +756,9 @@ public class LauncherApps { case MSG_UNSUSPENDED: mCallback.onPackagesUnsuspended(info.packageNames, info.user); break; + case MSG_SHORTCUT_CHANGED: + mCallback.onShortcutsChanged(info.packageName, info.shortcuts, info.user); + break; } } @@ -582,5 +814,14 @@ public class LauncherApps { info.user = user; obtainMessage(MSG_UNSUSPENDED, info).sendToTarget(); } + + public void postOnShortcutChanged(String packageName, UserHandle user, + List<ShortcutInfo> shortcuts) { + CallbackInfo info = new CallbackInfo(); + info.packageName = packageName; + info.user = user; + info.shortcuts = shortcuts; + obtainMessage(MSG_SHORTCUT_CHANGED, info).sendToTarget(); + } } } diff --git a/libs/hwui/tests/unit/CrashHandlerInjector.cpp b/core/java/android/content/pm/ShortcutInfo.aidl index b1c678d1c6fa..08e18731aa9b 100644 --- a/libs/hwui/tests/unit/CrashHandlerInjector.cpp +++ b/core/java/android/content/pm/ShortcutInfo.aidl @@ -13,29 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.content.pm; -#include "tests/common/TestUtils.h" - -#include <gtest/gtest.h> -#include <cstdio> - -using namespace android::uirenderer; - -static void gunitCrashHandler() { - auto testinfo = ::testing::UnitTest::GetInstance()->current_test_info(); - printf("[ FAILED ] %s.%s\n", testinfo->test_case_name(), - testinfo->name()); - printf("[ FATAL! ] RenderThread crashed, aborting tests!\n"); - fflush(stdout); -} - -static void hookError() { - TestUtils::setRenderThreadCrashHandler(gunitCrashHandler); -} - -class HookErrorInit { -public: - HookErrorInit() { hookError(); } -}; - -static HookErrorInit sInit; +parcelable ShortcutInfo; diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java new file mode 100644 index 000000000000..65205636c98e --- /dev/null +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -0,0 +1,680 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * TODO Enhance javadoc + * + * Represents a shortcut form an application. + * + * Notes... + * - If an {@link Icon} is of a resource, then we'll just persist the package name and resource ID. + * + * Otherwise, the bitmap will be fetched when it's registered to ShortcutManager, then *shrunk* + * if necessary, and persisted. + * + * We will disallow byte[] icons, because they can easily go over binder size limit. + * + * TODO Move save/load to this class + */ +public class ShortcutInfo implements Parcelable { + /* @hide */ + public static final int FLAG_DYNAMIC = 1 << 0; + + /* @hide */ + public static final int FLAG_PINNED = 1 << 1; + + /* @hide */ + public static final int FLAG_HAS_ICON_RES = 1 << 2; + + /* @hide */ + public static final int FLAG_HAS_ICON_FILE = 1 << 3; + + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_DYNAMIC, + FLAG_PINNED, + FLAG_HAS_ICON_RES, + FLAG_HAS_ICON_FILE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ShortcutFlags {} + + // Cloning options. + + /* @hide */ + private static final int CLONE_REMOVE_ICON = 1 << 0; + + /* @hide */ + private static final int CLONE_REMOVE_INTENT = 1 << 1; + + /* @hide */ + public static final int CLONE_REMOVE_NON_KEY_INFO = 1 << 2; + + /* @hide */ + public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON; + + /* @hide */ + public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT; + + /** @hide */ + @IntDef(flag = true, + value = { + CLONE_REMOVE_ICON, + CLONE_REMOVE_INTENT, + CLONE_REMOVE_NON_KEY_INFO, + CLONE_REMOVE_FOR_CREATOR, + CLONE_REMOVE_FOR_LAUNCHER + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CloneFlags {} + + private final String mId; + + @NonNull + private final String mPackageName; + + @Nullable + private ComponentName mActivityComponent; + + @Nullable + private Icon mIcon; + + @NonNull + private String mTitle; + + @NonNull + private Intent mIntent; + + // Internal use only. + @NonNull + private PersistableBundle mIntentPersistableExtras; + + private int mWeight; + + @Nullable + private PersistableBundle mExtras; + + private long mLastChangedTimestamp; + + // Internal use only. + @ShortcutFlags + private int mFlags; + + // Internal use only. + private int mIconResourceId; + + // Internal use only. + @Nullable + private String mBitmapPath; + + private ShortcutInfo(Builder b) { + mId = Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided"); + + // Note we can't do other null checks here because SM.updateShortcuts() takes partial + // information. + mPackageName = b.mContext.getPackageName(); + mActivityComponent = b.mActivityComponent; + mIcon = b.mIcon; + mTitle = b.mTitle; + mIntent = b.mIntent; + mWeight = b.mWeight; + mExtras = b.mExtras; + updateTimestamp(); + } + + /** + * Throws if any of the mandatory fields is not set. + * + * @hide + */ + public void enforceMandatoryFields() { + Preconditions.checkStringNotEmpty(mTitle, "Shortcut title must be provided"); + Preconditions.checkNotNull(mIntent, "Shortcut Intent must be provided"); + } + + /** + * Copy constructor. + */ + private ShortcutInfo(ShortcutInfo source, @CloneFlags int cloneFlags) { + mId = source.mId; + mPackageName = source.mPackageName; + mActivityComponent = source.mActivityComponent; + mFlags = source.mFlags; + mLastChangedTimestamp = source.mLastChangedTimestamp; + + if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) { + if ((cloneFlags & CLONE_REMOVE_ICON) == 0) { + mIcon = source.mIcon; + } + + mTitle = source.mTitle; + if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) { + mIntent = source.mIntent; + mIntentPersistableExtras = source.mIntentPersistableExtras; + } + mWeight = source.mWeight; + mExtras = source.mExtras; + mIconResourceId = source.mIconResourceId; + mBitmapPath = source.mBitmapPath; + } + } + + /** + * Copy a {@link ShortcutInfo}, optionally removing fields. + * @hide + */ + public ShortcutInfo clone(@CloneFlags int cloneFlags) { + return new ShortcutInfo(this, cloneFlags); + } + + /** + * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information + * will be overwritten. The timestamp will be updated. + * + * - Flags will not change + * - mBitmapPath will not change + * - Current time will be set to timestamp + * + * @hide + */ + public void copyNonNullFieldsFrom(ShortcutInfo source) { + Preconditions.checkState(mId == source.mId, "ID must match"); + Preconditions.checkState(mPackageName.equals(source.mPackageName), + "Package namae must match"); + + if (source.mActivityComponent != null) { + mActivityComponent = source.mActivityComponent; + } + + if (source.mIcon != null) { + mIcon = source.mIcon; + } + if (source.mTitle != null) { + mTitle = source.mTitle; + } + if (source.mIntent != null) { + mIntent = source.mIntent; + mIntentPersistableExtras = source.mIntentPersistableExtras; + } + if (source.mWeight != 0) { + mWeight = source.mWeight; + } + if (source.mExtras != null) { + mExtras = source.mExtras; + } + + updateTimestamp(); + } + + /** + * Builder class for {@link ShortcutInfo} objects. + */ + public static class Builder { + private final Context mContext; + + private String mId; + + private ComponentName mActivityComponent; + + private Icon mIcon; + + private String mTitle; + + private Intent mIntent; + + private int mWeight; + + private PersistableBundle mExtras; + + /** Constructor. */ + public Builder(Context context) { + mContext = context; + } + + /** + * Sets the ID of the shortcut. This is a mandatory field. + */ + @NonNull + public Builder setId(@NonNull String id) { + mId = Preconditions.checkStringNotEmpty(id, "id"); + return this; + } + + /** + * Optionally sets the target activity. + */ + @NonNull + public Builder setActivityComponent(@NonNull ComponentName activityComponent) { + mActivityComponent = Preconditions.checkNotNull(activityComponent, "activityComponent"); + return this; + } + + /** + * Optionally sets an icon. + * + * - Tint is not supported TODO Either check and throw, or support it. + * - URI icons will be converted into Bitmap icons at the registration time. + * + * TODO Only allow Bitmap, Resource and URI types. byte[] type can easily go over + * binder size limit. + */ + @NonNull + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets the title of a shortcut. This is a mandatory field. + */ + @NonNull + public Builder setTitle(@NonNull String title) { + mTitle = Preconditions.checkStringNotEmpty(title, "title"); + return this; + } + + /** + * Sets the intent of a shortcut. This is a mandatory field. The extras must only contain + * persistable information. (See {@link PersistableBundle}). + */ + @NonNull + public Builder setIntent(@NonNull Intent intent) { + mIntent = Preconditions.checkNotNull(intent, "intent"); + return this; + } + + /** + * Optionally sets the weight of a shortcut, which will be used by Launcher for sorting. + * The larger the weight, the more "important" a shortcut is. + */ + @NonNull + public Builder setWeight(int weight) { + mWeight = weight; + return this; + } + + /** + * Optional values that application can set. + * TODO: reserve keys starting with "android." + */ + @NonNull + public Builder setExtras(@NonNull PersistableBundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates a {@link ShortcutInfo} instance. + */ + @NonNull + public ShortcutInfo build() { + return new ShortcutInfo(this); + } + } + + /** + * Return the ID of the shortcut. + */ + @NonNull + public String getId() { + return mId; + } + + /** + * Return the ID of the shortcut. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Return the target activity, which may be null, in which case the shortcut is not associated + * with a specific activity. + */ + @Nullable + public ComponentName getActivityComponent() { + return mActivityComponent; + } + + /** + * Icon. + * + * For performance reasons, this will <b>NOT</b> be available when an instance is returned + * by {@link ShortcutManager} or {@link LauncherApps}. A launcher application needs to use + * other APIs in LauncherApps to fetch the bitmap. TODO Add a precondition for it. + * + * @hide + */ + @Nullable + public Icon getIcon() { + return mIcon; + } + + /** + * Return the shortcut title. + */ + @NonNull + public String getTitle() { + return mTitle; + } + + /** + * Return the intent. + * TODO Set mIntentPersistableExtras and before returning. + */ + @NonNull + public Intent getIntent() { + return mIntent; + } + + /** @hide */ + @Nullable + public PersistableBundle getIntentPersistableExtras() { + return mIntentPersistableExtras; + } + + /** + * Return the weight of a shortcut, which will be used by Launcher for sorting. + * The larger the weight, the more "important" a shortcut is. + */ + public int getWeight() { + return mWeight; + } + + /** + * Optional values that application can set. + */ + @Nullable + public PersistableBundle getExtras() { + return mExtras; + } + + /** + * Last time when any of the fields was updated. + */ + public long getLastChangedTimestamp() { + return mLastChangedTimestamp; + } + + /** @hide */ + @ShortcutFlags + public int getFlags() { + return mFlags; + } + + /** @hide*/ + public void setFlags(@ShortcutFlags int flags) { + mFlags = flags; + } + + /** @hide*/ + public void addFlags(@ShortcutFlags int flags) { + mFlags |= flags; + } + + /** @hide*/ + public void clearFlags(@ShortcutFlags int flags) { + mFlags &= ~flags; + } + + /** @hide*/ + public boolean hasFlags(@ShortcutFlags int flags) { + return (mFlags & flags) == flags; + } + + /** Return whether a shortcut is dynamic. */ + public boolean isDynamic() { + return hasFlags(FLAG_DYNAMIC); + } + + /** Return whether a shortcut is pinned. */ + public boolean isPinned() { + return hasFlags(FLAG_PINNED); + } + + /** + * Return whether a shortcut's icon is a resource in the owning package. + * + * @see LauncherApps#getShortcutIconResId(ShortcutInfo, UserHandle) + */ + public boolean hasIconResource() { + return hasFlags(FLAG_HAS_ICON_RES); + } + + /** + * Return whether a shortcut's icon is stored as a file. + * + * @see LauncherApps#getShortcutIconFd(ShortcutInfo, UserHandle) + */ + public boolean hasIconFile() { + return hasFlags(FLAG_HAS_ICON_FILE); + } + + /** @hide */ + public void updateTimestamp() { + mLastChangedTimestamp = System.currentTimeMillis(); + } + + /** @hide */ + // VisibleForTesting + public void setTimestamp(long value) { + mLastChangedTimestamp = value; + } + + /** @hide */ + public void setIcon(Icon icon) { + mIcon = icon; + } + + /** @hide */ + public void setTitle(String title) { + mTitle = title; + } + + /** @hide */ + public void setIntent(Intent intent) { + mIntent = intent; + } + + /** @hide */ + public void setIntentPersistableExtras(PersistableBundle intentPersistableExtras) { + mIntentPersistableExtras = intentPersistableExtras; + } + + /** @hide */ + public void setWeight(int weight) { + mWeight = weight; + } + + /** @hide */ + public void setExtras(PersistableBundle extras) { + mExtras = extras; + } + + /** @hide */ + public int getIconResourceId() { + return mIconResourceId; + } + + /** @hide */ + public String getBitmapPath() { + return mBitmapPath; + } + + /** @hide */ + public void setBitmapPath(String bitmapPath) { + mBitmapPath = bitmapPath; + } + + private ShortcutInfo(Parcel source) { + final ClassLoader cl = getClass().getClassLoader(); + + mId = source.readString(); + mPackageName = source.readString(); + mActivityComponent = source.readParcelable(cl); + mIcon = source.readParcelable(cl); + mTitle = source.readString(); + mIntent = source.readParcelable(cl); + mIntentPersistableExtras = source.readParcelable(cl); + mWeight = source.readInt(); + mExtras = source.readParcelable(cl); + mLastChangedTimestamp = source.readLong(); + mFlags = source.readInt(); + mIconResourceId = source.readInt(); + mBitmapPath = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeString(mPackageName); + dest.writeParcelable(mActivityComponent, flags); + dest.writeParcelable(mIcon, flags); + dest.writeString(mTitle); + dest.writeParcelable(mIntent, flags); + dest.writeParcelable(mIntentPersistableExtras, flags); + dest.writeInt(mWeight); + dest.writeParcelable(mExtras, flags); + dest.writeLong(mLastChangedTimestamp); + dest.writeInt(mFlags); + dest.writeInt(mIconResourceId); + dest.writeString(mBitmapPath); + } + + public static final Creator<ShortcutInfo> CREATOR = + new Creator<ShortcutInfo>() { + public ShortcutInfo createFromParcel(Parcel source) { + return new ShortcutInfo(source); + } + public ShortcutInfo[] newArray(int size) { + return new ShortcutInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * Return a string representation, intended for logging. Some fields will be retracted. + */ + @Override + public String toString() { + return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false); + } + + /** @hide */ + public String toInsecureString() { + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true); + } + + private String toStringInner(boolean secure, boolean includeInternalData) { + final StringBuilder sb = new StringBuilder(); + sb.append("ShortcutInfo {"); + + sb.append("id="); + sb.append(secure ? "***" : mId); + + sb.append(", packageName="); + sb.append(mPackageName); + + if (isDynamic()) { + sb.append(", dynamic"); + } + if (isPinned()) { + sb.append(", pinned"); + } + + sb.append(", activity="); + sb.append(mActivityComponent); + + sb.append(", title="); + sb.append(secure ? "***" : mTitle); + + sb.append(", icon="); + sb.append(mIcon); + + sb.append(", weight="); + sb.append(mWeight); + + sb.append(", timestamp="); + sb.append(mLastChangedTimestamp); + + sb.append(", intent="); + sb.append(mIntent); + + sb.append(", intentExtras="); + sb.append(secure ? "***" : mIntentPersistableExtras); + + sb.append(", extras="); + sb.append(mExtras); + + if (includeInternalData) { + sb.append(", flags="); + sb.append(mFlags); + + sb.append(", iconRes="); + sb.append(mIconResourceId); + + sb.append(", bitmapPath="); + sb.append(mBitmapPath); + } + + sb.append("}"); + return sb.toString(); + } + + /** @hide */ + public ShortcutInfo(String id, String packageName, ComponentName activityComponent, + Icon icon, String title, Intent intent, PersistableBundle intentPersistableExtras, + int weight, PersistableBundle extras, long lastChangedTimestamp, + int flags, int iconResId, String bitmapPath) { + mId = id; + mPackageName = packageName; + mActivityComponent = activityComponent; + mIcon = icon; + mTitle = title; + mIntent = intent; + mIntentPersistableExtras = intentPersistableExtras; + mWeight = weight; + mExtras = extras; + mLastChangedTimestamp = lastChangedTimestamp; + mFlags = flags; + mIconResourceId = iconResId; + mBitmapPath = bitmapPath; + } +} diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java new file mode 100644 index 000000000000..4c51d499ab4a --- /dev/null +++ b/core/java/android/content/pm/ShortcutManager.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; + +/** + * TODO Enhance javadoc + * + * {@link ShortcutManager} manages shortcuts created by applications. + * + * <h3>Dynamic shortcuts and pinned shortcuts</h3> + * + * An application can publish shortcuts with {@link #setDynamicShortcuts(List)} and + * {@link #addDynamicShortcut(ShortcutInfo)}. There can be at most + * {@link #getMaxDynamicShortcutCount()} number of dynamic shortcuts at a time from the same + * application. + * A dynamic shortcut can be deleted with {@link #deleteDynamicShortcut(String)}, and apps + * can also use {@link #deleteAllDynamicShortcuts()} to delete all dynamic shortcuts. + * + * <p>The shortcuts that are currently published by the above APIs are called "dynamic", because + * they can be removed by the creator application at any time. The user may "pin" dynamic shortcuts + * on Launcher to make "pinned" shortcuts. Pinned shortcuts <b>cannot</b> be removed by the creator + * app. An application can obtain all pinned shortcuts from itself with + * {@link #getPinnedShortcuts()}. Applications should keep the pinned shortcut information + * up-to-date using {@link #updateShortcuts(List)}. + * + * <p>The number of pinned shortcuts does not affect the number of dynamic shortcuts that can be + * published by an application at a time. + * No matter how many pinned shortcuts that Launcher has for an application, the + * application can still always publish {@link #getMaxDynamicShortcutCount()} number of dynamic + * shortcuts. + * + * <h3>Shortcut IDs</h3> + * + * Each shortcut must have an ID, which must be unique within each application. When a shortcut is + * published, existing shortcuts with the same ID will be updated. Note this may include a + * pinned shortcut. + * + * <h3>Rate limiting</h3> + * + * Calls to {@link #setDynamicShortcuts(List)}, {@link #addDynamicShortcut(ShortcutInfo)}, + * and {@link #updateShortcuts(List)} will be + * rate-limited. An application can call these methods at most + * {@link #getRemainingCallCount()} times until the rate-limiting counter is reset, + * which happens at a certain time every day. + * + * <p>An applications can use {@link #getRateLimitResetTime()} to get the next reset time. + * + * <h3>Backup and Restore</h3> + * + * Shortcuts will be backed up and restored across devices. This means all information, including + * IDs, must be meaningful on a different device. + * + * TODO: Define a Broadcast to let apps update shortcuts on a restored device. + * + * <h3>APIs for launcher</h3> + * + * Launcher applications should use {@link LauncherApps} to get shortcuts that are published from + * applications. Launcher applications can also pin shortcuts with + * {@link LauncherApps#pinShortcuts(String, List, UserHandle)}. + */ +public class ShortcutManager { + private static final String TAG = "ShortcutManager"; + + private final Context mContext; + private final IShortcutService mService; + + /** + * @hide + */ + public ShortcutManager(Context context, IShortcutService service) { + mContext = context; + mService = service; + } + + /** + * Publish a list of shortcuts. All existing dynamic shortcuts from the caller application + * will be replaced. + * + * <p>This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + * + * @throws IllegalArgumentException if {@code shortcutInfoList} contains more than + * {@link #getMaxDynamicShortcutCount()} shortcuts. + */ + public boolean setDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) { + try { + return mService.setDynamicShortcuts(mContext.getPackageName(), + new ParceledListSlice(shortcutInfoList), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all dynamic shortcuts from the caller application. The number of result items + * will not exceed the value returned by {@link #getMaxDynamicShortcutCount()}. + */ + @NonNull + public List<ShortcutInfo> getDynamicShortcuts() { + try { + return mService.getDynamicShortcuts(mContext.getPackageName(), injectMyUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Publish a single dynamic shortcut. If there's already dynamic or pinned shortcuts with + * the same ID, they will all be updated. + * + * <p>This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + * + * @throws IllegalArgumentException if the caller application has already published the + * max number of dynamic shortcuts. + */ + public boolean addDynamicShortcut(@NonNull ShortcutInfo shortcutInfo) { + try { + return mService.addDynamicShortcut( + mContext.getPackageName(), shortcutInfo, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Delete a single dynamic shortcut by ID. + */ + public void deleteDynamicShortcut(@NonNull String shortcutId) { + try { + mService.deleteDynamicShortcut(mContext.getPackageName(), shortcutId, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Delete all dynamic shortcuts from the caller application. + */ + public void deleteAllDynamicShortcuts() { + try { + mService.deleteAllDynamicShortcuts(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all pinned shortcuts from the caller application. + */ + @NonNull + public List<ShortcutInfo> getPinnedShortcuts() { + try { + return mService.getPinnedShortcuts(mContext.getPackageName(), injectMyUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Update all existing shortcuts with the same IDs. Shortcuts may be pinned and/or dynamic. + * + * <p>This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + */ + public boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) { + try { + return mService.updateShortcuts(mContext.getPackageName(), + new ParceledListSlice(shortcutInfoList), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the max number of dynamic shortcuts that each application can have at a time. + */ + public int getMaxDynamicShortcutCount() { + try { + return mService.getMaxDynamicShortcutCount(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the number of times the caller application can call the rate-limited APIs + * before the rate limit counter is reset. + * + * @see #getRateLimitResetTime() + */ + public int getRemainingCallCount() { + try { + return mService.getRemainingCallCount(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return when the rate limit count will be reset next time, in milliseconds since the epoch. + * + * @see #getRemainingCallCount() + * @see System#currentTimeMillis() + */ + public long getRateLimitResetTime() { + try { + return mService.getRateLimitResetTime(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide injection point */ + @VisibleForTesting + protected int injectMyUserId() { + return UserHandle.myUserId(); + } +} diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java new file mode 100644 index 000000000000..3d6028a6ffe5 --- /dev/null +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.LauncherApps.ShortcutQuery; + +import java.util.List; + +/** + * Entry points used by {@link LauncherApps}. + * + * <p>No permission / argument checks will be performed inside. + * Callers must check the calling app permission and the calling package name. + * @hide + */ +public abstract class ShortcutServiceInternal { + public interface ShortcutChangeListener { + void onShortcutChanged(@NonNull String packageName, + @NonNull List<ShortcutInfo> shortcuts, @UserIdInt int userId); + } + + public abstract List<ShortcutInfo> + getShortcuts(@NonNull String callingPackage, long changedSince, + @Nullable String packageName, @Nullable ComponentName componentName, + @ShortcutQuery.QueryFlags int flags, + int userId); + + public abstract List<ShortcutInfo> + getShortcutInfo(@NonNull String callingPackage, + @NonNull String packageName, @Nullable List<String> ids, int userId); + + public abstract void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, + @NonNull List<String> shortcutIds, int userId); + + public abstract Intent createShortcutIntent(@NonNull String callingPackage, + @NonNull String packageName, @NonNull String shortcutId, int userId); + + public abstract void addListener(@NonNull ShortcutChangeListener listener); +} diff --git a/core/java/android/content/res/GradientColor.java b/core/java/android/content/res/GradientColor.java index 98ef2eaa5e6d..cc46cbd43258 100644 --- a/core/java/android/content/res/GradientColor.java +++ b/core/java/android/content/res/GradientColor.java @@ -17,6 +17,7 @@ package android.content.res; import android.annotation.ColorInt; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources.Theme; @@ -37,13 +38,48 @@ import android.util.Log; import android.util.Xml; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; - +/** + * Lets you define a gradient color, which is used inside + * {@link android.graphics.drawable.VectorDrawable}. + * + * {@link android.content.res.GradientColor}s are created from XML resource files defined in the + * "color" subdirectory directory of an application's resource directory. The XML file contains + * a single "gradient" element with a number of attributes and elements inside. For example: + * <pre> + * <gradient xmlns:android="http://schemas.android.com/apk/res/android"> + * <android:startColor="?android:attr/colorPrimary"/> + * <android:endColor="?android:attr/colorControlActivated"/> + * <.../> + * <android:type="linear"/> + * </gradient> + * </pre> + * + * This can describe either a {@link android.graphics.LinearGradient}, + * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}. + * + * Note that different attributes are relevant for different types of gradient. + * For example, android:gradientRadius is only applied to RadialGradient. + * androd:centerX and android:centerY are only applied to SweepGradient or RadialGradient. + * android:startX, android:startY, android:endX and android:endY are only applied to LinearGradient. + * + * Also note if any color "item" element is defined, then startColor, centerColor and endColor will + * be ignored. + */ public class GradientColor extends ComplexColor { private static final String TAG = "GradientColor"; private static final boolean DBG_GRADIENT = false; + @IntDef({TILE_MODE_CLAMP, TILE_MODE_REPEAT, TILE_MODE_MIRROR}) + @Retention(RetentionPolicy.SOURCE) + private @interface GradientTileMode {} + private static final int TILE_MODE_CLAMP = 0; + private static final int TILE_MODE_REPEAT = 1; + private static final int TILE_MODE_MIRROR = 2; + /** Lazily-created factory for this GradientColor. */ private GradientColorFactory mFactory; @@ -54,7 +90,8 @@ public class GradientColor extends ComplexColor { // all the XML information. private Shader mShader = null; - // Below are the attributes at the root element <gradient> + // Below are the attributes at the root element <gradient>. + // NOTE: they need to be copied in the copy constructor! private int mGradientType = GradientDrawable.LINEAR_GRADIENT; private float mCenterX = 0f; @@ -70,6 +107,8 @@ public class GradientColor extends ComplexColor { private int mEndColor = 0; private boolean mHasCenterColor = false; + private int mTileMode = 0; // Clamp mode. + private float mGradientRadius = 0f; // Below are the attributes for the <item> element. @@ -100,6 +139,7 @@ public class GradientColor extends ComplexColor { mEndColor = copy.mEndColor; mHasCenterColor = copy.mHasCenterColor; mGradientRadius = copy.mGradientRadius; + mTileMode = copy.mTileMode; if (copy.mItemColors != null) { mItemColors = copy.mItemColors.clone(); @@ -117,6 +157,20 @@ public class GradientColor extends ComplexColor { } } + // Set the default to clamp mode. + private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) { + switch (tileMode) { + case TILE_MODE_CLAMP: + return Shader.TileMode.CLAMP; + case TILE_MODE_REPEAT: + return Shader.TileMode.REPEAT; + case TILE_MODE_MIRROR: + return Shader.TileMode.MIRROR; + default: + return Shader.TileMode.CLAMP; + } + } + /** * Update the root level's attributes, either for inflate or applyTheme. */ @@ -150,6 +204,9 @@ public class GradientColor extends ComplexColor { mEndColor = a.getColor( R.styleable.GradientColor_endColor, mEndColor); + mTileMode = a.getInt( + R.styleable.GradientColor_tileMode, mTileMode); + if (DBG_GRADIENT) { Log.v(TAG, "hasCenterColor is " + mHasCenterColor); if (mHasCenterColor) { @@ -157,6 +214,7 @@ public class GradientColor extends ComplexColor { } Log.v(TAG, "startColor: " + mStartColor); Log.v(TAG, "endColor: " + mEndColor); + Log.v(TAG, "tileMode: " + mTileMode); } mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius, @@ -406,11 +464,11 @@ public class GradientColor extends ComplexColor { if (mGradientType == GradientDrawable.LINEAR_GRADIENT) { mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets, - Shader.TileMode.CLAMP); + parseTileMode(mTileMode)); } else { if (mGradientType == GradientDrawable.RADIAL_GRADIENT) { mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors, - tempOffsets, Shader.TileMode.CLAMP); + tempOffsets, parseTileMode(mTileMode)); } else { mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets); } diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index ee7bd9acec77..5c7137341cd3 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -235,6 +235,12 @@ public class BaseBundle { return mParcelledData != null; } + /** @hide */ + ArrayMap<String, Object> getMap() { + unparcel(); + return mMap; + } + /** * Returns the number of mappings contained in this Bundle. * diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index ea180b216c63..f36bb2989c72 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -24,9 +24,6 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; /** * A mapping from String values to various types that can be saved to persistent and later @@ -82,6 +79,18 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa super(b); } + + /** + * Constructs a PersistableBundle from a Bundle. + * + * @param b a Bundle to be copied. + * + * @throws IllegalArgumentException if any element of {@code b} cannot be persisted. + */ + public PersistableBundle(Bundle b) { + this(b.getMap()); + } + /** * Constructs a PersistableBundle containing the mappings passed in. * @@ -101,6 +110,8 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa if (value instanceof ArrayMap) { // Fix up any Maps by replacing them with PersistableBundles. mMap.setValueAt(i, new PersistableBundle((ArrayMap<String, Object>) value)); + } else if (value instanceof Bundle) { + mMap.setValueAt(i, new PersistableBundle(((Bundle) value))); } else if (!isValidType(value)) { throw new IllegalArgumentException("Bad value in PersistableBundle key=" + mMap.keyAt(i) + " value=" + value); diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index ff370422e5a1..9467c228bb9c 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -49,6 +49,7 @@ public class EditTextPreference extends DialogPreference { private EditText mEditText; private String mText; + private boolean mTextSet; public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @@ -85,15 +86,16 @@ public class EditTextPreference extends DialogPreference { * @param text The text to save */ public void setText(String text) { - final boolean wasBlocking = shouldDisableDependents(); - - mText = text; - - persistString(text); - - final boolean isBlocking = shouldDisableDependents(); - if (isBlocking != wasBlocking) { - notifyDependencyChange(isBlocking); + // Always persist/notify the first time. + final boolean changed = !TextUtils.equals(mText, text); + if (changed || !mTextSet) { + mText = text; + mTextSet = true; + persistString(text); + if(changed) { + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } } } diff --git a/core/java/android/provider/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java index 0439fe247ee1..def303acf94c 100644 --- a/core/java/android/provider/BlockedNumberContract.java +++ b/core/java/android/provider/BlockedNumberContract.java @@ -29,11 +29,14 @@ import android.os.Bundle; * <p> * The content provider exposes a table containing blocked numbers. The columns and URIs for * accessing this table are defined by the {@link BlockedNumbers} class. Messages, and calls from - * blocked numbers are discarded by the platform. If the user contacts emergency - * services, number blocking is disabled by the platform for a duration defined by + * blocked numbers are discarded by the platform. Notifications upon provider changes can be + * received using a {@link android.database.ContentObserver}. + * </p> + * <p> + * The platform will not block messages, and calls from emergency numbers as defined by + * {@link android.telephony.PhoneNumberUtils#isEmergencyNumber(String)}. If the user contacts + * emergency services, number blocking is disabled by the platform for a duration defined by * {@link android.telephony.CarrierConfigManager#KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT}. - * Notifications upon provider changes can be received using a - * {@link android.database.ContentObserver}. * </p> * * <h3> Permissions </h3> diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java index 11737c65314e..0163b47f2ba7 100644 --- a/core/java/android/service/notification/Condition.java +++ b/core/java/android/service/notification/Condition.java @@ -16,32 +16,56 @@ package android.service.notification; +import android.annotation.IntDef; import android.annotation.SystemApi; +import android.app.AutomaticZenRule; import android.content.Context; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** - * Condition information from condition providers. Used to tell the system to enter Do Not Disturb - * mode and request that the system exit Do Not Disturb mode. + * The current condition of an {@link android.app.AutomaticZenRule}, provided by the + * {@link ConditionProviderService} that owns the rule. Used to tell the system to enter Do Not + * Disturb mode and request that the system exit Do Not Disturb mode. */ public class Condition implements Parcelable { + @SystemApi public static final String SCHEME = "condition"; + /** @hide */ + @IntDef({STATE_FALSE, STATE_TRUE, STATE_TRUE, STATE_ERROR}) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + /** + * Indicates that Do Not Disturb should be turned off. Note that all Conditions from all + * {@link ConditionProviderService} providers must be off for Do Not Disturb to be turned off on + * the device. + */ public static final int STATE_FALSE = 0; + /** + * Indicates that Do Not Disturb should be turned on. + */ public static final int STATE_TRUE = 1; + + @SystemApi public static final int STATE_UNKNOWN = 2; + @SystemApi public static final int STATE_ERROR = 3; + @SystemApi public static final int FLAG_RELEVANT_NOW = 1 << 0; + @SystemApi public static final int FLAG_RELEVANT_ALWAYS = 1 << 1; /** - * The URI representing the condition being updated. + * The URI representing the rule being updated. * See {@link android.app.AutomaticZenRule#getConditionId()}. */ public final Uri id; @@ -52,23 +76,17 @@ public class Condition implements Parcelable { */ public final String summary; - /** - * Additional information about what the rule encoded in {@link #id} means when it is enabled. - * User visible if the state of the condition is {@link #STATE_TRUE}. - */ + @SystemApi public final String line1; - - /** - * Additional information about what the rule encoded in {@link #id} means when it is enabled. - * User visible if the state of the condition is {@link #STATE_TRUE}. - */ + @SystemApi public final String line2; /** - * The state of this condition. {@link #STATE_TRUE} will enable Do Not Disturb mode. Any other - * state will turn Do Not Disturb off for this rule. Note that Do Not Disturb might still be - * enabled globally if other conditions are in a {@link #STATE_TRUE} state. + * The state of this condition. {@link #STATE_TRUE} will enable Do Not Disturb mode. + * {@link #STATE_FALSE} will turn Do Not Disturb off for this rule. Note that Do Not Disturb + * might still be enabled globally if other conditions are in a {@link #STATE_TRUE} state. */ + @State public final int state; @SystemApi @@ -76,8 +94,13 @@ public class Condition implements Parcelable { @SystemApi public final int icon; - public Condition(Uri id, String summary, String line1, String line2, int state) { - this(id, summary, line1, line2, -1, state, FLAG_RELEVANT_ALWAYS); + /** + * An object representing the current state of a {@link android.app.AutomaticZenRule}. + * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule + * @param summary a user visible description of the rule state. + */ + public Condition(Uri id, String summary, int state) { + this(id, summary, "", "", -1, state, FLAG_RELEVANT_ALWAYS); } @SystemApi @@ -85,8 +108,6 @@ public class Condition implements Parcelable { int state, int flags) { if (id == null) throw new IllegalArgumentException("id is required"); if (summary == null) throw new IllegalArgumentException("summary is required"); - if (line1 == null) throw new IllegalArgumentException("line1 is required"); - if (line2 == null) throw new IllegalArgumentException("line2 is required"); if (!isValidState(state)) throw new IllegalArgumentException("state is invalid: " + state); this.id = id; this.summary = summary; @@ -97,7 +118,7 @@ public class Condition implements Parcelable { this.flags = flags; } - private Condition(Parcel source) { + public Condition(Parcel source) { this((Uri)source.readParcelable(Condition.class.getClassLoader()), source.readString(), source.readString(), @@ -135,6 +156,7 @@ public class Condition implements Parcelable { .append(']').toString(); } + @SystemApi public static String stateToString(int state) { if (state == STATE_FALSE) return "STATE_FALSE"; if (state == STATE_TRUE) return "STATE_TRUE"; @@ -143,6 +165,7 @@ public class Condition implements Parcelable { throw new IllegalArgumentException("state is invalid: " + state); } + @SystemApi public static String relevanceToString(int flags) { final boolean now = (flags & FLAG_RELEVANT_NOW) != 0; final boolean always = (flags & FLAG_RELEVANT_ALWAYS) != 0; @@ -175,6 +198,7 @@ public class Condition implements Parcelable { return 0; } + @SystemApi public Condition copy() { final Parcel parcel = Parcel.obtain(); try { @@ -186,10 +210,14 @@ public class Condition implements Parcelable { } } + @SystemApi public static Uri.Builder newId(Context context) { - return new Uri.Builder().scheme(SCHEME).authority(context.getPackageName()); + return new Uri.Builder() + .scheme(Condition.SCHEME) + .authority(context.getPackageName()); } + @SystemApi public static boolean isValidId(Uri id, String pkg) { return id != null && SCHEME.equals(id.getScheme()) && pkg.equals(id.getAuthority()); } diff --git a/core/java/android/service/notification/ConditionProviderService.java b/core/java/android/service/notification/ConditionProviderService.java index adcc9d688cda..44c3887ec35d 100644 --- a/core/java/android/service/notification/ConditionProviderService.java +++ b/core/java/android/service/notification/ConditionProviderService.java @@ -102,9 +102,6 @@ public abstract class ConditionProviderService extends Service { */ abstract public void onConnected(); - /** - * @removed - */ @SystemApi public void onRequestConditions(int relevance) {} diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 4afa9fef9912..316c7e3f6084 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -41,12 +41,12 @@ import android.view.animation.AnimationUtils; import android.widget.ListView; import android.widget.Spinner; +import com.android.internal.R; + import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; -import com.android.internal.R; - /** * A Transition holds information about animations that will be run on its * targets during a scene change. Subclasses of this abstract class may @@ -192,7 +192,7 @@ public abstract class Transition implements Cloneable { private TransitionValuesMaps mStartValues = new TransitionValuesMaps(); private TransitionValuesMaps mEndValues = new TransitionValuesMaps(); TransitionSet mParent = null; - private int[] mMatchOrder = DEFAULT_MATCH_ORDER; + int[] mMatchOrder = DEFAULT_MATCH_ORDER; ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts @@ -246,7 +246,7 @@ public abstract class Transition implements Cloneable { // The function used to interpolate along two-dimensional points. Typically used // for adding curves to x/y View motion. - private PathMotion mPathMotion = STRAIGHT_PATH_MOTION; + PathMotion mPathMotion = STRAIGHT_PATH_MOTION; /** * Constructs a Transition object with no target objects. A transition with @@ -780,7 +780,7 @@ public abstract class Transition implements Cloneable { } } } - if (minStartDelay != 0) { + if (startDelays.size() != 0) { for (int i = 0; i < startDelays.size(); i++) { int index = startDelays.keyAt(i); Animator animator = mAnimators.get(index); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 6e025161f4ce..17f19912d31b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -135,6 +135,18 @@ public interface WindowManager extends ViewManager { } /** + * Message for taking fullscreen screenshot + * @hide + */ + final int TAKE_SCREENSHOT_FULLSCREEN = 1; + + /** + * Message for taking screenshot of selected region. + * @hide + */ + final int TAKE_SCREENSHOT_SELECTED_REGION = 2; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; @@ -269,6 +281,7 @@ public interface WindowManager extends ViewManager { @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION_STARTING, to = "TYPE_VOICE_INTERACTION_STARTING"), @ViewDebug.IntToString(from = TYPE_DOCK_DIVIDER, to = "TYPE_DOCK_DIVIDER"), @ViewDebug.IntToString(from = TYPE_QS_DIALOG, to = "TYPE_QS_DIALOG"), + @ViewDebug.IntToString(from = TYPE_SCREENSHOT, to = "TYPE_SCREENSHOT") }) public int type; @@ -622,6 +635,13 @@ public interface WindowManager extends ViewManager { public static final int TYPE_QS_DIALOG = FIRST_SYSTEM_WINDOW+35; /** + * Window type: shares similar characteristics with {@link #TYPE_DREAM}. The layer is + * reserved for screenshot region selection. + * @hide + */ + public static final int TYPE_SCREENSHOT = FIRST_SYSTEM_WINDOW + 36; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; diff --git a/core/java/android/view/textservice/SpellCheckerInfo.java b/core/java/android/view/textservice/SpellCheckerInfo.java index 471b6d4322f7..fc17f7aed301 100644 --- a/core/java/android/view/textservice/SpellCheckerInfo.java +++ b/core/java/android/view/textservice/SpellCheckerInfo.java @@ -31,10 +31,12 @@ import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; +import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.Xml; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; /** @@ -55,7 +57,7 @@ public final class SpellCheckerInfo implements Parcelable { /** * The array of subtypes. */ - private final ArrayList<SpellCheckerSubtype> mSubtypes = new ArrayList<SpellCheckerSubtype>(); + private final ArrayList<SpellCheckerSubtype> mSubtypes = new ArrayList<>(); /** * Constructor. @@ -267,4 +269,22 @@ public final class SpellCheckerInfo implements Parcelable { public int describeContents() { return 0; } + + /** + * @hide + */ + public void dump(final PrintWriter pw, final String prefix) { + pw.println(prefix + "mId=" + mId); + pw.println(prefix + "mSettingsActivityName=" + mSettingsActivityName); + pw.println(prefix + "Service:"); + mService.dump(new PrintWriterPrinter(pw), prefix + " "); + final int N = getSubtypeCount(); + for (int i = 0; i < N; i++) { + final SpellCheckerSubtype st = getSubtypeAt(i); + pw.println(prefix + " " + "Subtype #" + i + ":"); + pw.println(prefix + " " + "locale=" + st.getLocale() + + " languageTag=" + st.getLanguageTag()); + pw.println(prefix + " " + "extraValue=" + st.getExtraValue()); + } + } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 90a573ba8fbd..9ccd7f0a77dd 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8170,6 +8170,8 @@ i Defined in same coordinates as the path itself --> <attr name="endY" format="float" /> + <!-- Defines the tile mode of the gradient. SweepGradient don't support tiling. --> + <attr name="tileMode"/> </declare-styleable> <!-- Describes an item of a GradientColor. Minimally need 2 items to define the gradient diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 1d7034df9a57..17afd92b648a 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4228,4 +4228,9 @@ <!-- The representation of a time duration when negative. An example is -1:14. This can be used with a countdown timer for example.--> <string name="negative_duration">\u2212<xliff:g id="time" example="1:14">%1$s</xliff:g></string> + <!-- Title of notification shown when device has been forced to safe mode after a security compromise. --> + <string name="audit_safemode_notification">Factory reset to use this device normally</string> + <!-- Description of notification shown when device has been forced to safe mode after a security compromise. --> + <string name="audit_safemode_notification_details">Touch to learn more.</string> + </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 012012830211..cad0e7b4570e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1895,6 +1895,8 @@ <java-symbol type="string" name="config_customVpnConfirmDialogComponent" /> <java-symbol type="string" name="config_defaultNetworkScorerPackageName" /> <java-symbol type="string" name="config_persistentDataPackageName" /> + <java-symbol type="string" name="audit_safemode_notification" /> + <java-symbol type="string" name="audit_safemode_notification_details" /> <java-symbol type="layout" name="resolver_list" /> <java-symbol type="id" name="resolver_list" /> diff --git a/core/tests/coretests/src/android/transition/TransitionTest.java b/core/tests/coretests/src/android/transition/TransitionTest.java new file mode 100644 index 000000000000..7e72e25b6f45 --- /dev/null +++ b/core/tests/coretests/src/android/transition/TransitionTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.transition; + +import android.animation.AnimatorSetActivity; +import android.app.Activity; +import android.graphics.Rect; +import android.test.ActivityInstrumentationTestCase2; +import android.transition.Transition.EpicenterCallback; +import android.util.ArrayMap; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.TextView; + +import com.android.frameworks.coretests.R; + +public class TransitionTest extends ActivityInstrumentationTestCase2<AnimatorSetActivity> { + Activity mActivity; + public TransitionTest() { + super(AnimatorSetActivity.class); + } + + @Override + protected void setUp() throws Exception { + mActivity = getActivity(); + } + + public void testClone() throws Throwable { + View square1 = mActivity.findViewById(R.id.square1); + View square2 = mActivity.findViewById(R.id.square2); + View square3 = mActivity.findViewById(R.id.square3); + Fade fade = new Fade(); + fade.setStartDelay(1000); + fade.setDuration(1001); + + fade.addTarget(square1); + fade.excludeTarget(square2, true); + fade.excludeChildren(square3, true); + + fade.addTarget(R.id.square4); + fade.excludeTarget(R.id.square3, true); + fade.excludeChildren(R.id.square2, true); + + fade.addTarget("hello"); + fade.excludeTarget("world", true); + + fade.addTarget(View.class); + fade.excludeTarget(TextView.class, true); + + fade.setMatchOrder(Transition.MATCH_ID); + fade.setPropagation(new CircularPropagation()); + fade.setPathMotion(new ArcMotion()); + fade.setInterpolator(new AccelerateInterpolator()); + fade.setNameOverrides(new ArrayMap<>()); + + EpicenterCallback epicenterCallback = new EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + return null; + } + }; + + fade.setEpicenterCallback(epicenterCallback); + + Fade clone = (Fade) fade.clone(); + assertEquals(fade.mStartDelay, clone.mStartDelay); + assertEquals(fade.mDuration, clone.mDuration); + assertEquals(fade.mInterpolator, clone.mInterpolator); + assertEquals(fade.mPropagation, clone.mPropagation); + assertEquals(fade.getPathMotion(), clone.getPathMotion()); + assertEquals(fade.getEpicenterCallback(), clone.getEpicenterCallback()); + assertEquals(fade.mNameOverrides, clone.mNameOverrides); + assertEquals(fade.mMatchOrder, clone.mMatchOrder); + + assertEquals(fade.mTargets, clone.mTargets); + assertEquals(fade.mTargetExcludes, clone.mTargetExcludes); + assertEquals(fade.mTargetChildExcludes, clone.mTargetChildExcludes); + + assertEquals(fade.mTargetIds, clone.mTargetIds); + assertEquals(fade.mTargetIdExcludes, clone.mTargetIdExcludes); + assertEquals(fade.mTargetIdChildExcludes, clone.mTargetIdChildExcludes); + + assertEquals(fade.mTargetNames, clone.mTargetNames); + assertEquals(fade.mTargetNameExcludes, clone.mTargetNameExcludes); + + assertEquals(fade.mTargetTypes, clone.mTargetTypes); + assertEquals(fade.mTargetTypeExcludes, clone.mTargetTypeExcludes); + } +} diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index 70995acbb4ca..9a9a89bba53f 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -227,15 +227,16 @@ include $(CLEAR_VARS) LOCAL_MODULE := hwui_unit_tests LOCAL_MODULE_TAGS := tests LOCAL_STATIC_LIBRARIES := libhwui_static_null_gpu +LOCAL_SHARED_LIBRARIES := libmemunreachable LOCAL_CFLAGS := \ $(hwui_cflags) \ -DHWUI_NULL_GPU LOCAL_SRC_FILES += \ $(hwui_test_common_src_files) \ + tests/unit/main.cpp \ tests/unit/CanvasStateTests.cpp \ tests/unit/ClipAreaTests.cpp \ - tests/unit/CrashHandlerInjector.cpp \ tests/unit/DamageAccumulatorTests.cpp \ tests/unit/DeviceInfoTests.cpp \ tests/unit/FatVectorTests.cpp \ diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 9595a85e99dd..c809ff4f85e8 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -19,9 +19,6 @@ #include "DeferredLayerUpdater.h" #include "LayerRenderer.h" -#include <unistd.h> -#include <signal.h> - namespace android { namespace uirenderer { @@ -136,27 +133,7 @@ void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text, canvas->drawTextOnPath(glyphs.data(), glyphs.size(), path, 0, 0, paint); } -static void defaultCrashHandler() { - fprintf(stderr, "RenderThread crashed!"); -} - -static std::function<void()> gCrashHandler = defaultCrashHandler; -static sighandler_t gPreviousSignalHandler; - -static void signalHandler(int sig) { - gCrashHandler(); - if (gPreviousSignalHandler) { - gPreviousSignalHandler(sig); - } -} - -void TestUtils::setRenderThreadCrashHandler(std::function<void()> crashHandler) { - gCrashHandler = crashHandler; -} - void TestUtils::TestTask::run() { - gPreviousSignalHandler = signal(SIGABRT, signalHandler); - // RenderState only valid once RenderThread is running, so queried here RenderState& renderState = renderthread::RenderThread::getInstance().renderState(); @@ -164,9 +141,6 @@ void TestUtils::TestTask::run() { rtCallback(renderthread::RenderThread::getInstance()); renderState.flush(Caches::FlushMode::Full); renderState.onGLContextDestroyed(); - - // Restore the previous signal handler - signal(SIGABRT, gPreviousSignalHandler); } } /* namespace uirenderer */ diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 6f237059dd33..28ac1166fc5c 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -180,8 +180,6 @@ public: typedef std::function<void(renderthread::RenderThread& thread)> RtCallback; - static void setRenderThreadCrashHandler(std::function<void()> crashHandler); - class TestTask : public renderthread::RenderTask { public: TestTask(RtCallback rtCallback) diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp new file mode 100644 index 000000000000..409a12d37693 --- /dev/null +++ b/libs/hwui/tests/unit/main.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gtest/gtest.h" + +#include "Caches.h" +#include "thread/TaskManager.h" +#include "tests/common/TestUtils.h" + +#include <memunreachable/memunreachable.h> + +#include <cstdio> +#include <iostream> +#include <map> +#include <unordered_set> +#include <signal.h> +#include <unistd.h> + +using namespace std; +using namespace android; +using namespace android::uirenderer; + +static auto CRASH_SIGNALS = { + SIGABRT, + SIGSEGV, + SIGBUS, +}; + +static map<int, struct sigaction> gSigChain; + +static void gtestSigHandler(int sig, siginfo_t* siginfo, void* context) { + auto testinfo = ::testing::UnitTest::GetInstance()->current_test_info(); + printf("[ FAILED ] %s.%s\n", testinfo->test_case_name(), + testinfo->name()); + printf("[ FATAL! ] Process crashed, aborting tests!\n"); + fflush(stdout); + + // restore the default sighandler and re-raise + struct sigaction sa = gSigChain[sig]; + sigaction(sig, &sa, nullptr); + raise(sig); +} + +static void logUnreachable(initializer_list<UnreachableMemoryInfo> infolist) { + // merge them all + UnreachableMemoryInfo merged; + unordered_set<uintptr_t> addrs; + merged.allocation_bytes = 0; + merged.leak_bytes = 0; + merged.num_allocations = 0; + merged.num_leaks = 0; + for (auto& info : infolist) { + // We'll be a little hazzy about these ones and just hope the biggest + // is the most accurate + merged.allocation_bytes = max(merged.allocation_bytes, info.allocation_bytes); + merged.num_allocations = max(merged.num_allocations, info.num_allocations); + for (auto& leak : info.leaks) { + if (addrs.find(leak.begin) == addrs.end()) { + merged.leaks.push_back(leak); + merged.num_leaks++; + merged.leak_bytes += leak.size; + addrs.insert(leak.begin); + } + } + } + + // Now log the result + if (merged.num_leaks) { + cout << endl << "Leaked memory!" << endl; + if (!merged.leaks[0].backtrace.num_frames) { + cout << "Re-run with 'setprop libc.debug.malloc.program hwui_unit_test'" + << endl << "and 'setprop libc.debug.malloc.options backtrace=8'" + << " to get backtraces" << endl; + } + cout << merged.ToString(false); + } +} + +static void checkForLeaks() { + // TODO: Until we can shutdown the RT thread we need to do this in + // two passes as GetUnreachableMemory has limited insight into + // thread-local caches so some leaks will not be properly tagged as leaks + nsecs_t before = systemTime(); + UnreachableMemoryInfo rtMemInfo; + TestUtils::runOnRenderThread([&rtMemInfo](renderthread::RenderThread& thread) { + if (Caches::hasInstance()) { + Caches::getInstance().tasks.stop(); + } + // Check for leaks + if (!GetUnreachableMemory(rtMemInfo)) { + cerr << "Failed to get unreachable memory!" << endl; + return; + } + }); + UnreachableMemoryInfo uiMemInfo; + if (!GetUnreachableMemory(uiMemInfo)) { + cerr << "Failed to get unreachable memory!" << endl; + return; + } + logUnreachable({rtMemInfo, uiMemInfo}); + nsecs_t after = systemTime(); + cout << "Leak check took " << ns2ms(after - before) << "ms" << endl; +} + +int main(int argc, char* argv[]) { + // Register a crash handler + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = >estSigHandler; + sa.sa_flags = SA_SIGINFO; + for (auto sig : CRASH_SIGNALS) { + struct sigaction old_sa; + sigaction(sig, &sa, &old_sa); + gSigChain.insert(pair<int, struct sigaction>(sig, old_sa)); + } + + // Run the tests + testing::InitGoogleTest(&argc, argv); + int ret = RUN_ALL_TESTS(); + checkForLeaks(); + return ret; +} + diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index b8f0717d7318..37714746ae69 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -1738,7 +1738,10 @@ public class AudioRecord implements AudioRouting // TODO remove: implementation calls directly into implementation of native_release() private native final void native_finalize(); - private native final void native_release(); + /** + * @hide + */ + public native final void native_release(); private native final int native_start(int syncEvent, int sessionId); diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index c48bfc532965..2aac2b32bc62 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -2794,7 +2794,10 @@ public class AudioTrack implements AudioRouting private native final void native_finalize(); - private native final void native_release(); + /** + * @hide + */ + public native final void native_release(); private native final void native_start(); diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java index c08f4bf0eb19..81cc03561d3f 100644 --- a/media/java/android/media/ImageReader.java +++ b/media/java/android/media/ImageReader.java @@ -335,7 +335,6 @@ public class ImageReader implements AutoCloseable { switch (status) { case ACQUIRE_SUCCESS: - si.createSurfacePlanes(); si.mIsImageValid = true; case ACQUIRE_NO_BUFS: case ACQUIRE_MAX_IMAGES: @@ -693,7 +692,7 @@ public class ImageReader implements AutoCloseable { width = ImageReader.this.getWidth(); break; default: - width = nativeGetWidth(mFormat); + width = nativeGetWidth(); } return width; } @@ -709,7 +708,7 @@ public class ImageReader implements AutoCloseable { height = ImageReader.this.getHeight(); break; default: - height = nativeGetHeight(mFormat); + height = nativeGetHeight(); } return height; } @@ -729,6 +728,10 @@ public class ImageReader implements AutoCloseable { @Override public Plane[] getPlanes() { throwISEIfImageIsInvalid(); + + if (mPlanes == null) { + mPlanes = nativeCreatePlanes(ImageReader.this.mNumPlanes, ImageReader.this.mFormat); + } // Shallow copy is fine. return mPlanes.clone(); } @@ -766,7 +769,8 @@ public class ImageReader implements AutoCloseable { } private void clearSurfacePlanes() { - if (mIsImageValid) { + // Image#getPlanes may not be called before the image is closed. + if (mIsImageValid && mPlanes != null) { for (int i = 0; i < mPlanes.length; i++) { if (mPlanes[i] != null) { mPlanes[i].clearBuffer(); @@ -776,32 +780,25 @@ public class ImageReader implements AutoCloseable { } } - private void createSurfacePlanes() { - mPlanes = new SurfacePlane[ImageReader.this.mNumPlanes]; - for (int i = 0; i < ImageReader.this.mNumPlanes; i++) { - mPlanes[i] = nativeCreatePlane(i, ImageReader.this.mFormat); - } - } private class SurfacePlane extends android.media.Image.Plane { - // SurfacePlane instance is created by native code when a new SurfaceImage is created - private SurfacePlane(int index, int rowStride, int pixelStride) { - mIndex = index; + // SurfacePlane instance is created by native code when SurfaceImage#getPlanes() is + // called + private SurfacePlane(int rowStride, int pixelStride, ByteBuffer buffer) { mRowStride = rowStride; mPixelStride = pixelStride; + mBuffer = buffer; + /** + * Set the byteBuffer order according to host endianness (native + * order), otherwise, the byteBuffer order defaults to + * ByteOrder.BIG_ENDIAN. + */ + mBuffer.order(ByteOrder.nativeOrder()); } @Override public ByteBuffer getBuffer() { - SurfaceImage.this.throwISEIfImageIsInvalid(); - if (mBuffer != null) { - return mBuffer; - } else { - mBuffer = SurfaceImage.this.nativeImageGetBuffer(mIndex, - ImageReader.this.mFormat); - // Set the byteBuffer order according to host endianness (native order), - // otherwise, the byteBuffer order defaults to ByteOrder.BIG_ENDIAN. - return mBuffer.order(ByteOrder.nativeOrder()); - } + throwISEIfImageIsInvalid(); + return mBuffer; } @Override @@ -837,7 +834,6 @@ public class ImageReader implements AutoCloseable { mBuffer = null; } - final private int mIndex; final private int mPixelStride; final private int mRowStride; @@ -860,10 +856,10 @@ public class ImageReader implements AutoCloseable { // If this image is detached from the ImageReader. private AtomicBoolean mIsDetached = new AtomicBoolean(false); - private synchronized native ByteBuffer nativeImageGetBuffer(int idx, int readerFormat); - private synchronized native SurfacePlane nativeCreatePlane(int idx, int readerFormat); - private synchronized native int nativeGetWidth(int format); - private synchronized native int nativeGetHeight(int format); + private synchronized native SurfacePlane[] nativeCreatePlanes(int numPlanes, + int readerFormat); + private synchronized native int nativeGetWidth(); + private synchronized native int nativeGetHeight(); private synchronized native int nativeGetFormat(int readerFormat); } diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java index 851d43674458..83a4f17ffccb 100644 --- a/media/java/android/media/ImageWriter.java +++ b/media/java/android/media/ImageWriter.java @@ -748,8 +748,8 @@ public class ImageWriter implements AutoCloseable { final private int mPixelStride; final private int mRowStride; - // SurfacePlane instance is created by native code when a new - // SurfaceImage is created + // SurfacePlane instance is created by native code when SurfaceImage#getPlanes() is + // called private SurfacePlane(int rowStride, int pixelStride, ByteBuffer buffer) { mRowStride = rowStride; mPixelStride = pixelStride; @@ -795,7 +795,7 @@ public class ImageWriter implements AutoCloseable { } - // this will create the SurfacePlane object and fill the information + // Create the SurfacePlane object and fill the information private synchronized native SurfacePlane[] nativeCreatePlanes(int numPlanes, int writerFmt); private synchronized native int nativeGetWidth(); diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index a33219583e3c..e8c50e354039 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -575,16 +575,11 @@ public final class TvContract { /** * The original network ID of this TV channel. * - * <p>This is used to identify the originating delivery system, if applicable. Use the same - * coding for {@code original_network_id} in the underlying broadcast standard if it is - * defined there (e.g. ETSI EN 300 468/TR 101 211 and ARIB STD-B10). If channels cannot be - * globally identified by 2-tuple {{@link #COLUMN_TRANSPORT_STREAM_ID}, - * {@link #COLUMN_SERVICE_ID}}, one must carefully assign a value to this field to form a - * unique 3-tuple identification {{@code COLUMN_ORIGINAL_NETWORK_ID}, - * {@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}} for its channels. + * <p>It is used to identify the originating delivery system, if applicable. Use the same + * coding for {@code original_network_id} for ETSI EN 300 468/TR 101 211 and ARIB STD-B10. * - * <p>This is a required field if the channel cannot be uniquely identified by a 2-tuple - * {{@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}}. + * <p>This is a required field only if the underlying broadcast standard defines the same + * name field. Otherwise, leave empty. * * <p>Type: INTEGER */ @@ -593,13 +588,13 @@ public final class TvContract { /** * The transport stream ID of this channel. * - * <p>This is used to identify the Transport Stream that contains the current channel from - * any other multiplex within a network, if applicable. Use the same coding for + * <p>It is used to identify the Transport Stream that contains the current channel from any + * other multiplex within a network, if applicable. Use the same coding for * {@code transport_stream_id} defined in ISO/IEC 13818-1 if the channel is transmitted via - * the MPEG Transport Stream as is the case for many digital broadcast standards. + * the MPEG Transport Stream. * - * <p>This is a required field if the current channel is transmitted via the MPEG Transport - * Stream. + * <p>This is a required field only if the current channel is transmitted via the MPEG + * Transport Stream. Leave empty otherwise. * * <p>Type: INTEGER */ @@ -608,15 +603,13 @@ public final class TvContract { /** * The service ID of this channel. * - * <p>This is used to identify the current service (roughly equivalent to channel) from any - * other service within the Transport Stream, if applicable. Use the same coding for - * {@code service_id} in the underlying broadcast standard if it is defined there (e.g. ETSI - * EN 300 468 and ARIB STD-B10) or {@code program_number} (which usually has the same value - * as {@code service_id}) in ISO/IEC 13818-1 if the channel is transmitted via the MPEG - * Transport Stream. + * <p>It is used to identify the current service, or channel from any other services within + * a given Transport Stream, if applicable. Use the same coding for {@code service_id} in + * ETSI EN 300 468 and ARIB STD-B10 or {@code program_number} in ISO/IEC 13818-1. * - * <p>This is a required field if the current channel is transmitted via the MPEG Transport - * Stream. + * <p>This is a required field only if the underlying broadcast standard defines the same + * name field, or the current channel is transmitted via the MPEG Transport Stream. Leave + * empty otherwise. * * <p>Type: INTEGER */ diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 8fb58b5ce068..bc20c1790de2 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -491,7 +491,7 @@ public abstract class TvInputService extends Service { * until this method is called. * * <p>The TV input service must call this method as soon as the content rendered onto its - * surface is ready for viewing. This method must be called each time {@link #onTune(Uri)} + * surface is ready for viewing. This method must be called each time {@link #onTune} * is called. * * @see #notifyVideoUnavailable @@ -837,14 +837,15 @@ public abstract class TvInputService extends Service { public abstract boolean onTune(Uri channelUri); /** - * Calls {@link #onTune(Uri)}. Override this method in order to handle {@code params}. + * Calls {@link #onTune(Uri)}. Override this method in order to handle domain-specific + * features that are only known between certain TV inputs and their clients. * * @param channelUri The URI of the channel. - * @param params The extra parameters from other applications. + * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped + * name, i.e. prefixed with a package name you own, so that different developers + * will not create conflicting keys. * @return {@code true} if the tuning was successful, {@code false} otherwise. - * @hide */ - @SystemApi public boolean onTune(Uri channelUri, Bundle params) { return onTune(channelUri); } @@ -1209,7 +1210,7 @@ public abstract class TvInputService extends Service { } /** - * Calls {@link #onTune}. + * Calls {@link #onTune(Uri, Bundle)}. */ void tune(Uri channelUri, Bundle params) { mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -1836,7 +1837,7 @@ public abstract class TvInputService extends Service { * a hardware TV Input (e.g. HDMI 1) and forward the application's surface to the session so * that the user can see the screen of the hardware TV Input when she tunes to a channel from * this TV input. The implementation of this class is expected to change the channel of the - * external set-top box via a proprietary protocol when {@link HardwareSession#onTune(Uri)} is + * external set-top box via a proprietary protocol when {@link HardwareSession#onTune} is * requested by the application. * * <p>Note that this class is not for inputs for internal hardware like built-in tuner and HDMI diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 5c4b52883391..96230769762c 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -18,6 +18,7 @@ package android.media.tv; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; import android.graphics.Canvas; @@ -56,7 +57,7 @@ import java.util.Queue; * TV inputs available on the system can be obtained by calling * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.) * - * <p>Once the application supplies the URI for a specific TV channel to {@link #tune(String, Uri)} + * <p>Once the application supplies the URI for a specific TV channel to {@link #tune} * method, it takes care of underlying service binding (and unbinding if the current TvView is * already bound to a service) and automatically allocates/deallocates resources needed. In addition * to a few essential methods to control how the contents are presented, it also provides a way to @@ -206,13 +207,18 @@ public class TvView extends ViewGroup { } /** - * Sets the Z order of a window owning the surface of this TvView above the normal TvView - * but below an application. + * Controls whether the TvView's surface is placed on top of another regular surface view in the + * window (but still behind the window itself). + * This is typically used to place overlays on top of an underlying TvView. * - * @see SurfaceView#setZOrderMediaOverlay - * @hide + * <p>Note that this must be set before the TvView's containing window is attached to the + * window manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + * + * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false} + * otherwise. */ - @SystemApi public void setZOrderMediaOverlay(boolean isMediaOverlay) { if (isMediaOverlay) { mWindowZOrder = ZORDER_MEDIA_OVERLAY; @@ -230,12 +236,18 @@ public class TvView extends ViewGroup { } /** - * Sets the Z order of a window owning the surface of this TvView on top of an application. + * Controls whether the TvView's surface is placed on top of its window. Normally it is placed + * behind the window, to allow it to (for the most part) appear to composite with the views in + * the hierarchy. By setting this, you cause it to be placed above the window. This means that + * none of the contents of the window this TvView is in will be visible on top of its surface. * - * @see SurfaceView#setZOrderOnTop - * @hide + * <p>Note that this must be set before the TvView's containing window is attached to the window + * manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + * + * @param onTop {@code true} to be on top of its window, {@code false} otherwise. */ - @SystemApi public void setZOrderOnTop(boolean onTop) { if (onTop) { mWindowZOrder = ZORDER_ON_TOP; @@ -280,14 +292,15 @@ public class TvView extends ViewGroup { } /** - * Tunes to a given channel. + * Tunes to a given channel. This can be used to provide domain-specific features that are only + * known between certain TvView applications and their TV inputs. * * @param inputId The ID of TV input for the given channel. * @param channelUri The URI of a channel. - * @param params Extra parameters. - * @hide + * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped + * name, i.e. prefixed with a package name you own, so that different developers will + * not create conflicting keys. */ - @SystemApi public void tune(String inputId, Uri channelUri, Bundle params) { if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); if (TextUtils.isEmpty(inputId)) { @@ -360,11 +373,8 @@ public class TvView extends ViewGroup { * * @param unblockedRating A TvContentRating to unblock. * @see TvInputService.Session#notifyContentBlocked(TvContentRating) - * @hide - * @deprecated Use {@link #unblockContent} instead. + * @removed */ - @Deprecated - @SystemApi public void requestUnblockContent(TvContentRating unblockedRating) { unblockContent(unblockedRating); } @@ -379,6 +389,7 @@ public class TvView extends ViewGroup { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS) public void unblockContent(TvContentRating unblockedRating) { if (mSession != null) { mSession.unblockContent(unblockedRating); @@ -539,7 +550,7 @@ public class TvView extends ViewGroup { } /** - * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)} for the current + * Calls {@link TvInputService.Session#onAppPrivateCommand(String, Bundle)} for the current * session. * * @param action The name of the private command to send. This <em>must</em> be a scoped name, @@ -893,7 +904,7 @@ public class TvView extends ViewGroup { /** * This is invoked when the channel of this TvView is changed by the underlying TV input - * without any {@link TvView#tune(String, Uri)} request. + * without any {@link TvView#tune} request. * * @param inputId The ID of the TV input bound to this view. * @param channelUri The URI of a channel. diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index 9e90a19f00cc..c3993ae20183 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -16,6 +16,7 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "ImageReader_JNI" +#include "android_media_Utils.h" #include <utils/Log.h> #include <utils/misc.h> #include <utils/List.h> @@ -23,7 +24,6 @@ #include <cstdio> -#include <gui/CpuConsumer.h> #include <gui/BufferItemConsumer.h> #include <gui/Surface.h> #include <camera3.h> @@ -37,8 +37,6 @@ #include <stdint.h> #include <inttypes.h> -#define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) - #define ANDROID_MEDIA_IMAGEREADER_CTX_JNI_ID "mNativeContext" #define ANDROID_MEDIA_SURFACEIMAGE_BUFFER_JNI_ID "mNativeBuffer" #define ANDROID_MEDIA_SURFACEIMAGE_TS_JNI_ID "mTimestamp" @@ -47,9 +45,6 @@ using namespace android; -enum { - IMAGE_READER_MAX_NUM_PLANES = 3, -}; enum { ACQUIRE_SUCCESS = 0, @@ -65,6 +60,7 @@ static struct { static struct { jfieldID mNativeBuffer; jfieldID mTimestamp; + jfieldID mPlanes; } gSurfaceImageClassInfo; static struct { @@ -89,21 +85,12 @@ public: virtual void onFrameAvailable(const BufferItem& item); - CpuConsumer::LockedBuffer* getLockedBuffer(); - void returnLockedBuffer(CpuConsumer::LockedBuffer* buffer); - - BufferItem* getOpaqueBuffer(); - void returnOpaqueBuffer(BufferItem* buffer); + BufferItem* getBufferItem(); + void returnBufferItem(BufferItem* buffer); - void setCpuConsumer(const sp<CpuConsumer>& consumer) { mConsumer = consumer; } - CpuConsumer* getCpuConsumer() { return mConsumer.get(); } - void setOpaqueConsumer(const sp<BufferItemConsumer>& consumer) { mOpaqueConsumer = consumer; } - BufferItemConsumer* getOpaqueConsumer() { return mOpaqueConsumer.get(); } - // This is the only opaque format exposed in the ImageFormat public API. - // Note that we do support CPU access for HAL_PIXEL_FORMAT_RAW_OPAQUE - // (ImageFormat#RAW_PRIVATE) so it doesn't count as opaque here. - bool isOpaque() { return mFormat == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED; } + void setBufferConsumer(const sp<BufferItemConsumer>& consumer) { mConsumer = consumer; } + BufferItemConsumer* getBufferConsumer() { return mConsumer.get(); } void setProducer(const sp<IGraphicBufferProducer>& producer) { mProducer = producer; } IGraphicBufferProducer* getProducer() { return mProducer.get(); } @@ -124,10 +111,8 @@ private: static JNIEnv* getJNIEnv(bool* needsDetach); static void detachJNI(); - List<CpuConsumer::LockedBuffer*> mBuffers; - List<BufferItem*> mOpaqueBuffers; - sp<CpuConsumer> mConsumer; - sp<BufferItemConsumer> mOpaqueConsumer; + List<BufferItem*> mBuffers; + sp<BufferItemConsumer> mConsumer; sp<IGraphicBufferProducer> mProducer; jobject mWeakThiz; jclass mClazz; @@ -140,12 +125,14 @@ private: JNIImageReaderContext::JNIImageReaderContext(JNIEnv* env, jobject weakThiz, jclass clazz, int maxImages) : mWeakThiz(env->NewGlobalRef(weakThiz)), - mClazz((jclass)env->NewGlobalRef(clazz)) { + mClazz((jclass)env->NewGlobalRef(clazz)), + mFormat(0), + mDataSpace(HAL_DATASPACE_UNKNOWN), + mWidth(-1), + mHeight(-1) { for (int i = 0; i < maxImages; i++) { - CpuConsumer::LockedBuffer *buffer = new CpuConsumer::LockedBuffer; - BufferItem* opaqueBuffer = new BufferItem; + BufferItem* buffer = new BufferItem; mBuffers.push_back(buffer); - mOpaqueBuffers.push_back(opaqueBuffer); } } @@ -174,36 +161,21 @@ void JNIImageReaderContext::detachJNI() { } } -CpuConsumer::LockedBuffer* JNIImageReaderContext::getLockedBuffer() { +BufferItem* JNIImageReaderContext::getBufferItem() { if (mBuffers.empty()) { return NULL; } - // Return a LockedBuffer pointer and remove it from the list - List<CpuConsumer::LockedBuffer*>::iterator it = mBuffers.begin(); - CpuConsumer::LockedBuffer* buffer = *it; + // Return a BufferItem pointer and remove it from the list + List<BufferItem*>::iterator it = mBuffers.begin(); + BufferItem* buffer = *it; mBuffers.erase(it); return buffer; } -void JNIImageReaderContext::returnLockedBuffer(CpuConsumer::LockedBuffer* buffer) { +void JNIImageReaderContext::returnBufferItem(BufferItem* buffer) { mBuffers.push_back(buffer); } -BufferItem* JNIImageReaderContext::getOpaqueBuffer() { - if (mOpaqueBuffers.empty()) { - return NULL; - } - // Return an opaque buffer pointer and remove it from the list - List<BufferItem*>::iterator it = mOpaqueBuffers.begin(); - BufferItem* buffer = *it; - mOpaqueBuffers.erase(it); - return buffer; -} - -void JNIImageReaderContext::returnOpaqueBuffer(BufferItem* buffer) { - mOpaqueBuffers.push_back(buffer); -} - JNIImageReaderContext::~JNIImageReaderContext() { bool needsDetach = false; JNIEnv* env = getJNIEnv(&needsDetach); @@ -217,25 +189,15 @@ JNIImageReaderContext::~JNIImageReaderContext() { detachJNI(); } - // Delete LockedBuffers - for (List<CpuConsumer::LockedBuffer *>::iterator it = mBuffers.begin(); + // Delete buffer items. + for (List<BufferItem *>::iterator it = mBuffers.begin(); it != mBuffers.end(); it++) { delete *it; } - // Delete opaque buffers - for (List<BufferItem *>::iterator it = mOpaqueBuffers.begin(); - it != mOpaqueBuffers.end(); it++) { - delete *it; - } - - mBuffers.clear(); if (mConsumer != 0) { mConsumer.clear(); } - if (mOpaqueConsumer != 0) { - mOpaqueConsumer.clear(); - } } void JNIImageReaderContext::onFrameAvailable(const BufferItem& /*item*/) @@ -257,11 +219,6 @@ void JNIImageReaderContext::onFrameAvailable(const BufferItem& /*item*/) extern "C" { -static bool isFormatOpaque(int format) { - // Only treat IMPLEMENTATION_DEFINED as an opaque format for now. - return format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED; -} - static JNIImageReaderContext* ImageReader_getContext(JNIEnv* env, jobject thiz) { JNIImageReaderContext *ctx; @@ -270,24 +227,6 @@ static JNIImageReaderContext* ImageReader_getContext(JNIEnv* env, jobject thiz) return ctx; } -static CpuConsumer* ImageReader_getCpuConsumer(JNIEnv* env, jobject thiz) -{ - ALOGV("%s:", __FUNCTION__); - JNIImageReaderContext* const ctx = ImageReader_getContext(env, thiz); - if (ctx == NULL) { - jniThrowRuntimeException(env, "ImageReaderContext is not initialized"); - return NULL; - } - - if (ctx->isOpaque()) { - jniThrowException(env, "java/lang/IllegalStateException", - "Opaque ImageReader doesn't support this method"); - return NULL; - } - - return ctx->getCpuConsumer(); -} - static IGraphicBufferProducer* ImageReader_getProducer(JNIEnv* env, jobject thiz) { ALOGV("%s:", __FUNCTION__); @@ -315,411 +254,7 @@ static void ImageReader_setNativeContext(JNIEnv* env, reinterpret_cast<jlong>(ctx.get())); } -static CpuConsumer::LockedBuffer* Image_getLockedBuffer(JNIEnv* env, jobject image) -{ - return reinterpret_cast<CpuConsumer::LockedBuffer*>( - env->GetLongField(image, gSurfaceImageClassInfo.mNativeBuffer)); -} - -static void Image_setBuffer(JNIEnv* env, jobject thiz, - const CpuConsumer::LockedBuffer* buffer) -{ - env->SetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer, reinterpret_cast<jlong>(buffer)); -} - -static void Image_setOpaqueBuffer(JNIEnv* env, jobject thiz, - const BufferItem* buffer) -{ - env->SetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer, reinterpret_cast<jlong>(buffer)); -} - -static uint32_t Image_getJpegSize(CpuConsumer::LockedBuffer* buffer, bool usingRGBAOverride) -{ - ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); - uint32_t size = 0; - uint32_t width = buffer->width; - uint8_t* jpegBuffer = buffer->data; - - if (usingRGBAOverride) { - width = (buffer->width + buffer->stride * (buffer->height - 1)) * 4; - } - - // First check for JPEG transport header at the end of the buffer - uint8_t* header = jpegBuffer + (width - sizeof(struct camera3_jpeg_blob)); - struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header); - if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID) { - size = blob->jpeg_size; - ALOGV("%s: Jpeg size = %d", __FUNCTION__, size); - } - - // failed to find size, default to whole buffer - if (size == 0) { - /* - * This is a problem because not including the JPEG header - * means that in certain rare situations a regular JPEG blob - * will be misidentified as having a header, in which case - * we will get a garbage size value. - */ - ALOGW("%s: No JPEG header detected, defaulting to size=width=%d", - __FUNCTION__, width); - size = width; - } - - return size; -} - -static bool usingRGBAToJpegOverride(int32_t bufferFormat, int32_t readerCtxFormat) { - return readerCtxFormat == HAL_PIXEL_FORMAT_BLOB && bufferFormat == HAL_PIXEL_FORMAT_RGBA_8888; -} - -static int32_t applyFormatOverrides(int32_t bufferFormat, int32_t readerCtxFormat) -{ - // Using HAL_PIXEL_FORMAT_RGBA_8888 gralloc buffers containing JPEGs to get around SW - // write limitations for some platforms (b/17379185). - if (usingRGBAToJpegOverride(bufferFormat, readerCtxFormat)) { - return HAL_PIXEL_FORMAT_BLOB; - } - return bufferFormat; -} - -static void Image_getLockedBufferInfo(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - uint8_t **base, uint32_t *size, int32_t readerFormat) -{ - ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); - ALOG_ASSERT(base != NULL, "base is NULL!!!"); - ALOG_ASSERT(size != NULL, "size is NULL!!!"); - ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0)); - - ALOGV("%s: buffer: %p", __FUNCTION__, buffer); - - uint32_t dataSize, ySize, cSize, cStride; - uint8_t *cb, *cr; - uint8_t *pData = NULL; - int bytesPerPixel = 0; - - dataSize = ySize = cSize = cStride = 0; - int32_t fmt = buffer->flexFormat; - - bool usingRGBAOverride = usingRGBAToJpegOverride(fmt, readerFormat); - fmt = applyFormatOverrides(fmt, readerFormat); - switch (fmt) { - case HAL_PIXEL_FORMAT_YCbCr_420_888: - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - buffer->dataCb : - buffer->dataCr; - // only map until last pixel - if (idx == 0) { - dataSize = buffer->stride * (buffer->height - 1) + buffer->width; - } else { - dataSize = buffer->chromaStride * (buffer->height / 2 - 1) + - buffer->chromaStep * (buffer->width / 2 - 1) + 1; - } - break; - // NV21 - case HAL_PIXEL_FORMAT_YCrCb_420_SP: - cr = buffer->data + (buffer->stride * buffer->height); - cb = cr + 1; - // only map until last pixel - ySize = buffer->width * (buffer->height - 1) + buffer->width; - cSize = buffer->width * (buffer->height / 2 - 1) + buffer->width - 1; - - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - cb: - cr; - - dataSize = (idx == 0) ? ySize : cSize; - break; - case HAL_PIXEL_FORMAT_YV12: - // Y and C stride need to be 16 pixel aligned. - LOG_ALWAYS_FATAL_IF(buffer->stride % 16, - "Stride is not 16 pixel aligned %d", buffer->stride); - - ySize = buffer->stride * buffer->height; - cStride = ALIGN(buffer->stride / 2, 16); - cr = buffer->data + ySize; - cSize = cStride * buffer->height / 2; - cb = cr + cSize; - - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - cb : - cr; - dataSize = (idx == 0) ? ySize : cSize; - break; - case HAL_PIXEL_FORMAT_Y8: - // Single plane, 8bpp. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - - pData = buffer->data; - dataSize = buffer->stride * buffer->height; - break; - case HAL_PIXEL_FORMAT_Y16: - bytesPerPixel = 2; - // Single plane, 16bpp, strides are specified in pixels, not in bytes - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - break; - case HAL_PIXEL_FORMAT_BLOB: - // Used for JPEG data, height must be 1, width == size, single plane. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - ALOG_ASSERT(buffer->height == 1, - "JPEG should has height value one but got %d", buffer->height); - - pData = buffer->data; - dataSize = Image_getJpegSize(buffer, usingRGBAOverride); - break; - case HAL_PIXEL_FORMAT_RAW16: - // Single plane 16bpp bayer data. - bytesPerPixel = 2; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - break; - case HAL_PIXEL_FORMAT_RAW_OPAQUE: - // Used for RAW_OPAQUE data, height must be 1, width == size, single plane. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - ALOG_ASSERT(buffer->height == 1, - "RAW_PRIVATE should has height value one but got %d", buffer->height); - pData = buffer->data; - dataSize = buffer->width; - break; - case HAL_PIXEL_FORMAT_RAW10: - // Single plane 10bpp bayer data. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - LOG_ALWAYS_FATAL_IF(buffer->width % 4, - "Width is not multiple of 4 %d", buffer->width); - LOG_ALWAYS_FATAL_IF(buffer->height % 2, - "Height is not even %d", buffer->height); - LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 10 / 8), - "stride (%d) should be at least %d", - buffer->stride, buffer->width * 10 / 8); - pData = buffer->data; - dataSize = buffer->stride * buffer->height; - break; - case HAL_PIXEL_FORMAT_RAW12: - // Single plane 10bpp bayer data. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - LOG_ALWAYS_FATAL_IF(buffer->width % 4, - "Width is not multiple of 4 %d", buffer->width); - LOG_ALWAYS_FATAL_IF(buffer->height % 2, - "Height is not even %d", buffer->height); - LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 12 / 8), - "stride (%d) should be at least %d", - buffer->stride, buffer->width * 12 / 8); - pData = buffer->data; - dataSize = buffer->stride * buffer->height; - break; - case HAL_PIXEL_FORMAT_RGBA_8888: - case HAL_PIXEL_FORMAT_RGBX_8888: - // Single plane, 32bpp. - bytesPerPixel = 4; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - break; - case HAL_PIXEL_FORMAT_RGB_565: - // Single plane, 16bpp. - bytesPerPixel = 2; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - break; - case HAL_PIXEL_FORMAT_RGB_888: - // Single plane, 24bpp. - bytesPerPixel = 3; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - break; - default: - jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", - "Pixel format: 0x%x is unsupported", fmt); - break; - } - - *base = pData; - *size = dataSize; -} - -static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t halReaderFormat) -{ - ALOGV("%s: buffer index: %d", __FUNCTION__, idx); - ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0), "Index is out of range:%d", idx); - - int pixelStride = 0; - ALOG_ASSERT(buffer != NULL, "buffer is NULL"); - - int32_t fmt = buffer->flexFormat; - - fmt = applyFormatOverrides(fmt, halReaderFormat); - - switch (fmt) { - case HAL_PIXEL_FORMAT_YCbCr_420_888: - pixelStride = (idx == 0) ? 1 : buffer->chromaStep; - break; - case HAL_PIXEL_FORMAT_YCrCb_420_SP: - pixelStride = (idx == 0) ? 1 : 2; - break; - case HAL_PIXEL_FORMAT_Y8: - // Single plane 8bpp data. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 1; - break; - case HAL_PIXEL_FORMAT_YV12: - pixelStride = 1; - break; - case HAL_PIXEL_FORMAT_BLOB: - case HAL_PIXEL_FORMAT_RAW10: - case HAL_PIXEL_FORMAT_RAW12: - // Blob is used for JPEG data, RAW10 and RAW12 is used for 10-bit and 12-bit raw data, - // those are single plane data with pixel stride 0 since they don't really have a - // well defined pixel stride - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 0; - break; - case HAL_PIXEL_FORMAT_Y16: - case HAL_PIXEL_FORMAT_RAW16: - case HAL_PIXEL_FORMAT_RGB_565: - // Single plane 16bpp data. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 2; - break; - case HAL_PIXEL_FORMAT_RGBA_8888: - case HAL_PIXEL_FORMAT_RGBX_8888: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 4; - break; - case HAL_PIXEL_FORMAT_RGB_888: - // Single plane, 24bpp. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 3; - break; - case HAL_PIXEL_FORMAT_RAW_OPAQUE: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pixelStride = 0; // RAW OPAQUE doesn't have pixel stride - break; - default: - jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", - "Pixel format: 0x%x is unsupported", fmt); - break; - } - - return pixelStride; -} - -static jint Image_imageGetRowStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t halReaderFormat) -{ - ALOGV("%s: buffer index: %d", __FUNCTION__, idx); - ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0)); - - int rowStride = 0; - ALOG_ASSERT(buffer != NULL, "buffer is NULL"); - - int32_t fmt = buffer->flexFormat; - - fmt = applyFormatOverrides(fmt, halReaderFormat); - - switch (fmt) { - case HAL_PIXEL_FORMAT_YCbCr_420_888: - rowStride = (idx == 0) ? buffer->stride : buffer->chromaStride; - break; - case HAL_PIXEL_FORMAT_YCrCb_420_SP: - rowStride = buffer->width; - break; - case HAL_PIXEL_FORMAT_YV12: - LOG_ALWAYS_FATAL_IF(buffer->stride % 16, - "Stride is not 16 pixel aligned %d", buffer->stride); - rowStride = (idx == 0) ? buffer->stride : ALIGN(buffer->stride / 2, 16); - break; - case HAL_PIXEL_FORMAT_BLOB: - // Blob is used for JPEG data. It is single plane and has 0 row stride and - // 0 pixel stride - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = 0; - break; - case HAL_PIXEL_FORMAT_RAW10: - case HAL_PIXEL_FORMAT_RAW12: - // RAW10 and RAW12 are used for 10-bit and 12-bit raw data, they are single plane - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = buffer->stride; - break; - case HAL_PIXEL_FORMAT_Y8: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - LOG_ALWAYS_FATAL_IF(buffer->stride % 16, - "Stride is not 16 pixel aligned %d", buffer->stride); - rowStride = buffer->stride; - break; - case HAL_PIXEL_FORMAT_Y16: - case HAL_PIXEL_FORMAT_RAW16: - // In native side, strides are specified in pixels, not in bytes. - // Single plane 16bpp bayer data. even width/height, - // row stride multiple of 16 pixels (32 bytes) - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - LOG_ALWAYS_FATAL_IF(buffer->stride % 16, - "Stride is not 16 pixel aligned %d", buffer->stride); - rowStride = buffer->stride * 2; - break; - case HAL_PIXEL_FORMAT_RGB_565: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = buffer->stride * 2; - break; - case HAL_PIXEL_FORMAT_RGBA_8888: - case HAL_PIXEL_FORMAT_RGBX_8888: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = buffer->stride * 4; - break; - case HAL_PIXEL_FORMAT_RGB_888: - // Single plane, 24bpp. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = buffer->stride * 3; - break; - case HAL_PIXEL_FORMAT_RAW_OPAQUE: - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - rowStride = 0; // RAW OPAQUE doesn't have row stride - break; - default: - ALOGE("%s Pixel format: 0x%x is unsupported", __FUNCTION__, fmt); - jniThrowException(env, "java/lang/UnsupportedOperationException", - "unsupported buffer format"); - break; - } - - return rowStride; -} - -static int Image_getBufferWidth(CpuConsumer::LockedBuffer* buffer) { - if (buffer == NULL) return -1; - - if (!buffer->crop.isEmpty()) { - return buffer->crop.getWidth(); - } - return buffer->width; -} - -static int Image_getBufferHeight(CpuConsumer::LockedBuffer* buffer) { - if (buffer == NULL) return -1; - - if (!buffer->crop.isEmpty()) { - return buffer->crop.getHeight(); - } - return buffer->height; -} - -// --------------------------Methods for opaque Image and ImageReader---------- - -static BufferItemConsumer* ImageReader_getOpaqueConsumer(JNIEnv* env, jobject thiz) +static BufferItemConsumer* ImageReader_getBufferConsumer(JNIEnv* env, jobject thiz) { ALOGV("%s:", __FUNCTION__); JNIImageReaderContext* const ctx = ImageReader_getContext(env, thiz); @@ -728,40 +263,21 @@ static BufferItemConsumer* ImageReader_getOpaqueConsumer(JNIEnv* env, jobject th return NULL; } - if (!ctx->isOpaque()) { - jniThrowException(env, "java/lang/IllegalStateException", - "Non-opaque ImageReader doesn't support this method"); - } + return ctx->getBufferConsumer(); +} - return ctx->getOpaqueConsumer(); +static void Image_setBufferItem(JNIEnv* env, jobject thiz, + const BufferItem* buffer) +{ + env->SetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer, reinterpret_cast<jlong>(buffer)); } -static BufferItem* Image_getOpaqueBuffer(JNIEnv* env, jobject image) +static BufferItem* Image_getBufferItem(JNIEnv* env, jobject image) { return reinterpret_cast<BufferItem*>( env->GetLongField(image, gSurfaceImageClassInfo.mNativeBuffer)); } -static int Image_getOpaqueBufferWidth(BufferItem* buffer) { - if (buffer == NULL) return -1; - - if (!buffer->mCrop.isEmpty()) { - return buffer->mCrop.getWidth(); - } - return buffer->mGraphicBuffer->getWidth(); -} - -static int Image_getOpaqueBufferHeight(BufferItem* buffer) { - if (buffer == NULL) return -1; - - if (!buffer->mCrop.isEmpty()) { - return buffer->mCrop.getHeight(); - } - - return buffer->mGraphicBuffer->getHeight(); -} - - // ---------------------------------------------------------------------------- @@ -784,6 +300,11 @@ static void ImageReader_classInit(JNIEnv* env, jclass clazz) "can't find android/graphics/ImageReader.%s", ANDROID_MEDIA_SURFACEIMAGE_TS_JNI_ID); + gSurfaceImageClassInfo.mPlanes = env->GetFieldID( + imageClazz, "mPlanes", "[Landroid/media/ImageReader$SurfaceImage$SurfacePlane;"); + LOG_ALWAYS_FATAL_IF(gSurfaceImageClassInfo.mPlanes == NULL, + "can't find android/media/ImageReader$ReaderSurfaceImage.mPlanes"); + gImageReaderClassInfo.mNativeContext = env->GetFieldID( clazz, ANDROID_MEDIA_IMAGEREADER_CTX_JNI_ID, "J"); LOG_ALWAYS_FATAL_IF(gImageReaderClassInfo.mNativeContext == NULL, @@ -800,7 +321,7 @@ static void ImageReader_classInit(JNIEnv* env, jclass clazz) // FindClass only gives a local reference of jclass object. gSurfacePlaneClassInfo.clazz = (jclass) env->NewGlobalRef(planeClazz); gSurfacePlaneClassInfo.ctor = env->GetMethodID(gSurfacePlaneClassInfo.clazz, "<init>", - "(Landroid/media/ImageReader$SurfaceImage;III)V"); + "(Landroid/media/ImageReader$SurfaceImage;IILjava/nio/ByteBuffer;)V"); LOG_ALWAYS_FATAL_IF(gSurfacePlaneClassInfo.ctor == NULL, "Can not find SurfacePlane constructor"); } @@ -831,81 +352,52 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, sp<IGraphicBufferProducer> gbProducer; sp<IGraphicBufferConsumer> gbConsumer; BufferQueue::createBufferQueue(&gbProducer, &gbConsumer); - sp<ConsumerBase> consumer; - sp<CpuConsumer> cpuConsumer; - sp<BufferItemConsumer> opaqueConsumer; + sp<BufferItemConsumer> bufferConsumer; String8 consumerName = String8::format("ImageReader-%dx%df%xm%d-%d-%d", width, height, format, maxImages, getpid(), createProcessUniqueId()); + uint32_t consumerUsage = GRALLOC_USAGE_SW_READ_OFTEN; + if (isFormatOpaque(nativeFormat)) { // Use the SW_READ_NEVER usage to tell producer that this format is not for preview or video // encoding. The only possibility will be ZSL output. - opaqueConsumer = - new BufferItemConsumer(gbConsumer, GRALLOC_USAGE_SW_READ_NEVER, maxImages, - /*controlledByApp*/true); - if (opaqueConsumer == NULL) { - jniThrowRuntimeException(env, "Failed to allocate native opaque consumer"); - return; - } - ctx->setOpaqueConsumer(opaqueConsumer); - opaqueConsumer->setName(consumerName); - consumer = opaqueConsumer; - } else { - cpuConsumer = new CpuConsumer(gbConsumer, maxImages, /*controlledByApp*/true); - // TODO: throw dvm exOutOfMemoryError? - if (cpuConsumer == NULL) { - jniThrowRuntimeException(env, "Failed to allocate native CpuConsumer"); - return; - } - ctx->setCpuConsumer(cpuConsumer); - cpuConsumer->setName(consumerName); - consumer = cpuConsumer; + consumerUsage = GRALLOC_USAGE_SW_READ_NEVER; } + bufferConsumer = new BufferItemConsumer(gbConsumer, consumerUsage, maxImages, + /*controlledByApp*/true); + if (bufferConsumer == nullptr) { + jniThrowExceptionFmt(env, "java/lang/RuntimeException", + "Failed to allocate native buffer consumer for format 0x%x", nativeFormat); + return; + } + ctx->setBufferConsumer(bufferConsumer); + bufferConsumer->setName(consumerName); ctx->setProducer(gbProducer); - consumer->setFrameAvailableListener(ctx); + bufferConsumer->setFrameAvailableListener(ctx); ImageReader_setNativeContext(env, thiz, ctx); ctx->setBufferFormat(nativeFormat); ctx->setBufferDataspace(nativeDataspace); ctx->setBufferWidth(width); ctx->setBufferHeight(height); - // Set the width/height/format/dataspace to the CpuConsumer - // TODO: below code can be simplified once b/19977701 is fixed. - if (isFormatOpaque(nativeFormat)) { - res = opaqueConsumer->setDefaultBufferSize(width, height); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set opaque consumer buffer size"); - return; - } - res = opaqueConsumer->setDefaultBufferFormat(nativeFormat); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set opaque consumer buffer format"); - } - res = opaqueConsumer->setDefaultBufferDataSpace(nativeDataspace); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set opaque consumer buffer dataSpace"); - } - } else { - res = cpuConsumer->setDefaultBufferSize(width, height); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set CpuConsumer buffer size"); - return; - } - res = cpuConsumer->setDefaultBufferFormat(nativeFormat); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set CpuConsumer buffer format"); - } - res = cpuConsumer->setDefaultBufferDataSpace(nativeDataspace); - if (res != OK) { - jniThrowException(env, "java/lang/IllegalStateException", - "Failed to set CpuConsumer buffer dataSpace"); - } + // Set the width/height/format/dataspace to the bufferConsumer. + res = bufferConsumer->setDefaultBufferSize(width, height); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Failed to set buffer consumer default size (%dx%d) for format 0x%x", + width, height, nativeFormat); + return; + } + res = bufferConsumer->setDefaultBufferFormat(nativeFormat); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Failed to set buffer consumer default format 0x%x", nativeFormat); + } + res = bufferConsumer->setDefaultBufferDataSpace(nativeDataspace); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Failed to set buffer consumer default dataSpace 0x%x", nativeDataspace); } } @@ -919,12 +411,8 @@ static void ImageReader_close(JNIEnv* env, jobject thiz) return; } - ConsumerBase* consumer = NULL; - if (ctx->isOpaque()) { - consumer = ImageReader_getOpaqueConsumer(env, thiz); - } else { - consumer = ImageReader_getCpuConsumer(env, thiz); - } + BufferItemConsumer* consumer = NULL; + consumer = ImageReader_getBufferConsumer(env, thiz); if (consumer != NULL) { consumer->abandon(); @@ -933,6 +421,39 @@ static void ImageReader_close(JNIEnv* env, jobject thiz) ImageReader_setNativeContext(env, thiz, NULL); } +static sp<Fence> Image_unlockIfLocked(JNIEnv* env, jobject image) { + ALOGV("%s", __FUNCTION__); + BufferItem* buffer = Image_getBufferItem(env, image); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return Fence::NO_FENCE; + } + + // Is locked? + bool wasBufferLocked = false; + jobject planes = NULL; + if (!isFormatOpaque(buffer->mGraphicBuffer->getPixelFormat())) { + planes = env->GetObjectField(image, gSurfaceImageClassInfo.mPlanes); + } + wasBufferLocked = (planes != NULL); + if (wasBufferLocked) { + status_t res = OK; + int fenceFd = -1; + if (wasBufferLocked) { + res = buffer->mGraphicBuffer->unlockAsync(&fenceFd); + if (res != OK) { + jniThrowRuntimeException(env, "unlock buffer failed"); + return Fence::NO_FENCE; + } + } + sp<Fence> releaseFence = new Fence(fenceFd); + return releaseFence; + ALOGV("Successfully unlocked the image"); + } + return Fence::NO_FENCE; +} + static void ImageReader_imageRelease(JNIEnv* env, jobject thiz, jobject image) { ALOGV("%s:", __FUNCTION__); @@ -942,174 +463,124 @@ static void ImageReader_imageRelease(JNIEnv* env, jobject thiz, jobject image) return; } - if (ctx->isOpaque()) { - BufferItemConsumer* opaqueConsumer = ctx->getOpaqueConsumer(); - BufferItem* opaqueBuffer = Image_getOpaqueBuffer(env, image); - opaqueConsumer->releaseBuffer(*opaqueBuffer); // Not using fence for now. - Image_setOpaqueBuffer(env, image, NULL); - ctx->returnOpaqueBuffer(opaqueBuffer); - ALOGV("%s: Opaque Image has been released", __FUNCTION__); - } else { - CpuConsumer* consumer = ctx->getCpuConsumer(); - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, image); - if (!buffer) { - // Release an already closed image is harmless. - return; - } - consumer->unlockBuffer(*buffer); - Image_setBuffer(env, image, NULL); - ctx->returnLockedBuffer(buffer); - ALOGV("%s: Image (format: 0x%x) has been released", __FUNCTION__, ctx->getBufferFormat()); + BufferItemConsumer* bufferConsumer = ctx->getBufferConsumer(); + BufferItem* buffer = Image_getBufferItem(env, image); + if (buffer == nullptr) { + // Release an already closed image is harmless. + return; } + + sp<Fence> releaseFence = Image_unlockIfLocked(env, image); + bufferConsumer->releaseBuffer(*buffer, releaseFence); + Image_setBufferItem(env, image, NULL); + ctx->returnBufferItem(buffer); + ALOGV("%s: Image (format: 0x%x) has been released", __FUNCTION__, ctx->getBufferFormat()); } -static jint ImageReader_opaqueImageSetup(JNIEnv* env, JNIImageReaderContext* ctx, jobject image) { +static jint ImageReader_imageSetup(JNIEnv* env, jobject thiz, jobject image) { ALOGV("%s:", __FUNCTION__); - if (ctx == NULL || !ctx->isOpaque()) { - jniThrowRuntimeException(env, "ImageReaderContext is not initialized"); + JNIImageReaderContext* ctx = ImageReader_getContext(env, thiz); + if (ctx == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageReader is not initialized or was already closed"); return -1; } - BufferItemConsumer* opaqueConsumer = ctx->getOpaqueConsumer(); - BufferItem* buffer = ctx->getOpaqueBuffer(); + BufferItemConsumer* bufferConsumer = ctx->getBufferConsumer(); + BufferItem* buffer = ctx->getBufferItem(); if (buffer == NULL) { ALOGW("Unable to acquire a buffer item, very likely client tried to acquire more than" " maxImages buffers"); return ACQUIRE_MAX_IMAGES; } - status_t res = opaqueConsumer->acquireBuffer(buffer, 0); + status_t res = bufferConsumer->acquireBuffer(buffer, 0); if (res != OK) { - ctx->returnOpaqueBuffer(buffer); - if (res == INVALID_OPERATION) { - // Max number of images were already acquired. - ALOGE("%s: Max number of buffers allowed are already acquired : %s (%d)", - __FUNCTION__, strerror(-res), res); - return ACQUIRE_MAX_IMAGES; - } else { - ALOGE("%s: Acquire image failed with error: %s (%d)", - __FUNCTION__, strerror(-res), res); - return ACQUIRE_NO_BUFFERS; - } - } - - // Set SurfaceImage instance member variables - Image_setOpaqueBuffer(env, image, buffer); - env->SetLongField(image, gSurfaceImageClassInfo.mTimestamp, - static_cast<jlong>(buffer->mTimestamp)); - - return ACQUIRE_SUCCESS; -} - -static jint ImageReader_lockedImageSetup(JNIEnv* env, JNIImageReaderContext* ctx, jobject image) { - CpuConsumer* consumer = ctx->getCpuConsumer(); - CpuConsumer::LockedBuffer* buffer = ctx->getLockedBuffer(); - if (buffer == NULL) { - ALOGW("Unable to acquire a lockedBuffer, very likely client tries to lock more than" - " maxImages buffers"); - return ACQUIRE_MAX_IMAGES; - } - status_t res = consumer->lockNextBuffer(buffer); - if (res != NO_ERROR) { - ctx->returnLockedBuffer(buffer); - if (res != BAD_VALUE /*no buffers*/) { - if (res == NOT_ENOUGH_DATA) { + ctx->returnBufferItem(buffer); + if (res != BufferQueue::NO_BUFFER_AVAILABLE) { + if (res == INVALID_OPERATION) { + // Max number of images were already acquired. + ALOGE("%s: Max number of buffers allowed are already acquired : %s (%d)", + __FUNCTION__, strerror(-res), res); return ACQUIRE_MAX_IMAGES; } else { - ALOGE("%s Fail to lockNextBuffer with error: %d ", - __FUNCTION__, res); - jniThrowExceptionFmt(env, "java/lang/AssertionError", - "Unknown error (%d) when we tried to lock buffer.", - res); + ALOGE("%s: Acquire image failed with some unknown error: %s (%d)", + __FUNCTION__, strerror(-res), res); + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Unknown error (%d) when we tried to acquire an image.", + res); + return ACQUIRE_NO_BUFFERS; } } + // This isn't really an error case, as the application may acquire buffer at any time. return ACQUIRE_NO_BUFFERS; } - if (buffer->flexFormat == HAL_PIXEL_FORMAT_YCrCb_420_SP) { - jniThrowException(env, "java/lang/UnsupportedOperationException", - "NV21 format is not supported by ImageReader"); - return -1; - } + // Add some extra checks for non-opaque formats. + if (!isFormatOpaque(ctx->getBufferFormat())) { + // Check if the left-top corner of the crop rect is origin, we currently assume this point is + // zero, will revisit this once this assumption turns out problematic. + Point lt = buffer->mCrop.leftTop(); + if (lt.x != 0 || lt.y != 0) { + jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", + "crop left top corner [%d, %d] need to be at origin", lt.x, lt.y); + return -1; + } - // Check if the left-top corner of the crop rect is origin, we currently assume this point is - // zero, will revist this once this assumption turns out problematic. - Point lt = buffer->crop.leftTop(); - if (lt.x != 0 || lt.y != 0) { - jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", - "crop left top corner [%d, %d] need to be at origin", lt.x, lt.y); - return -1; - } + // Check if the producer buffer configurations match what ImageReader configured. + int outputWidth = getBufferWidth(buffer); + int outputHeight = getBufferHeight(buffer); + + int imgReaderFmt = ctx->getBufferFormat(); + int imageReaderWidth = ctx->getBufferWidth(); + int imageReaderHeight = ctx->getBufferHeight(); + int bufferFormat = buffer->mGraphicBuffer->getPixelFormat(); + if ((bufferFormat != HAL_PIXEL_FORMAT_BLOB) && (imgReaderFmt != HAL_PIXEL_FORMAT_BLOB) && + (imageReaderWidth != outputWidth || imageReaderHeight != outputHeight)) { + ALOGV("%s: Producer buffer size: %dx%d, doesn't match ImageReader configured size: %dx%d", + __FUNCTION__, outputWidth, outputHeight, imageReaderWidth, imageReaderHeight); + } + if (imgReaderFmt != bufferFormat) { + if (imgReaderFmt == HAL_PIXEL_FORMAT_YCbCr_420_888 && + isPossiblyYUV(bufferFormat)) { + // Treat formats that are compatible with flexible YUV + // (HAL_PIXEL_FORMAT_YCbCr_420_888) as HAL_PIXEL_FORMAT_YCbCr_420_888. + ALOGV("%s: Treat buffer format to 0x%x as HAL_PIXEL_FORMAT_YCbCr_420_888", + __FUNCTION__, bufferFormat); + } else if (imgReaderFmt == HAL_PIXEL_FORMAT_BLOB && + bufferFormat == HAL_PIXEL_FORMAT_RGBA_8888) { + // Using HAL_PIXEL_FORMAT_RGBA_8888 Gralloc buffers containing JPEGs to get around + // SW write limitations for (b/17379185). + ALOGV("%s: Receiving JPEG in HAL_PIXEL_FORMAT_RGBA_8888 buffer.", __FUNCTION__); + } else { + // Return the buffer to the queue. No need to provide fence, as this buffer wasn't + // used anywhere yet. + bufferConsumer->releaseBuffer(*buffer); + ctx->returnBufferItem(buffer); + + // Throw exception + ALOGE("Producer output buffer format: 0x%x, ImageReader configured format: 0x%x", + bufferFormat, ctx->getBufferFormat()); + String8 msg; + msg.appendFormat("The producer output buffer format 0x%x doesn't " + "match the ImageReader's configured buffer format 0x%x.", + bufferFormat, ctx->getBufferFormat()); + jniThrowException(env, "java/lang/UnsupportedOperationException", + msg.string()); + return -1; + } + } - // Check if the producer buffer configurations match what ImageReader configured. - int outputWidth = Image_getBufferWidth(buffer); - int outputHeight = Image_getBufferHeight(buffer); - - int imgReaderFmt = ctx->getBufferFormat(); - int imageReaderWidth = ctx->getBufferWidth(); - int imageReaderHeight = ctx->getBufferHeight(); - if ((buffer->format != HAL_PIXEL_FORMAT_BLOB) && (imgReaderFmt != HAL_PIXEL_FORMAT_BLOB) && - (imageReaderWidth != outputWidth || imageReaderHeight != outputHeight)) { - ALOGV("%s: Producer buffer size: %dx%d, doesn't match ImageReader configured size: %dx%d", - __FUNCTION__, outputWidth, outputHeight, imageReaderWidth, imageReaderHeight); } - int bufFmt = buffer->format; - if (imgReaderFmt == HAL_PIXEL_FORMAT_YCbCr_420_888) { - bufFmt = buffer->flexFormat; - } - if (imgReaderFmt != bufFmt) { - if (imgReaderFmt == HAL_PIXEL_FORMAT_YCbCr_420_888 && (bufFmt == - HAL_PIXEL_FORMAT_YCrCb_420_SP || bufFmt == HAL_PIXEL_FORMAT_YV12)) { - // Special casing for when producer switches to a format compatible with flexible YUV - // (HAL_PIXEL_FORMAT_YCbCr_420_888). - ctx->setBufferFormat(bufFmt); - ALOGD("%s: Overriding buffer format YUV_420_888 to %x.", __FUNCTION__, bufFmt); - } else if (imgReaderFmt == HAL_PIXEL_FORMAT_BLOB && bufFmt == HAL_PIXEL_FORMAT_RGBA_8888) { - // Using HAL_PIXEL_FORMAT_RGBA_8888 gralloc buffers containing JPEGs to get around SW - // write limitations for (b/17379185). - ALOGD("%s: Receiving JPEG in HAL_PIXEL_FORMAT_RGBA_8888 buffer.", __FUNCTION__); - } else { - // Return the buffer to the queue. - consumer->unlockBuffer(*buffer); - ctx->returnLockedBuffer(buffer); - - // Throw exception - ALOGE("Producer output buffer format: 0x%x, ImageReader configured format: 0x%x", - buffer->format, ctx->getBufferFormat()); - String8 msg; - msg.appendFormat("The producer output buffer format 0x%x doesn't " - "match the ImageReader's configured buffer format 0x%x.", - bufFmt, ctx->getBufferFormat()); - jniThrowException(env, "java/lang/UnsupportedOperationException", - msg.string()); - return -1; - } - } // Set SurfaceImage instance member variables - Image_setBuffer(env, image, buffer); + Image_setBufferItem(env, image, buffer); env->SetLongField(image, gSurfaceImageClassInfo.mTimestamp, - static_cast<jlong>(buffer->timestamp)); + static_cast<jlong>(buffer->mTimestamp)); return ACQUIRE_SUCCESS; } -static jint ImageReader_imageSetup(JNIEnv* env, jobject thiz, jobject image) { - ALOGV("%s:", __FUNCTION__); - JNIImageReaderContext* ctx = ImageReader_getContext(env, thiz); - if (ctx == NULL) { - jniThrowException(env, "java/lang/IllegalStateException", - "ImageReader is not initialized or was already closed"); - return -1; - } - - if (ctx->isOpaque()) { - return ImageReader_opaqueImageSetup(env, ctx, image); - } else { - return ImageReader_lockedImageSetup(env, ctx, image); - } -} - static jint ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image) { ALOGV("%s:", __FUNCTION__); JNIImageReaderContext* ctx = ImageReader_getContext(env, thiz); @@ -1118,29 +589,23 @@ static jint ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image) { return -1; } - status_t res = OK; - if (!ctx->isOpaque()) { - // TODO: Non-Opaque format detach is not implemented yet. - jniThrowRuntimeException(env, - "nativeDetachImage is not implemented yet for non-opaque format !!!"); - return -1; - } - - BufferItemConsumer* opaqueConsumer = ctx->getOpaqueConsumer(); - BufferItem* opaqueBuffer = Image_getOpaqueBuffer(env, image); - if (!opaqueBuffer) { + BufferItemConsumer* bufferConsumer = ctx->getBufferConsumer(); + BufferItem* buffer = Image_getBufferItem(env, image); + if (!buffer) { ALOGE( - "Opaque Image already released and can not be detached from ImageReader!!!"); + "Image already released and can not be detached from ImageReader!!!"); jniThrowException(env, "java/lang/IllegalStateException", - "Opaque Image detach from ImageReader failed: buffer was already released"); + "Image detach from ImageReader failed: buffer was already released"); return -1; } - res = opaqueConsumer->detachBuffer(opaqueBuffer->mSlot); + status_t res = OK; + Image_unlockIfLocked(env, image); + res = bufferConsumer->detachBuffer(buffer->mSlot); if (res != OK) { - ALOGE("Opaque Image detach failed: %s (%d)!!!", strerror(-res), res); + ALOGE("Image detach failed: %s (%d)!!!", strerror(-res), res); jniThrowRuntimeException(env, - "nativeDetachImage failed for opaque image!!!"); + "nativeDetachImage failed for image!!!"); return res; } return OK; @@ -1152,7 +617,7 @@ static jobject ImageReader_getSurface(JNIEnv* env, jobject thiz) IGraphicBufferProducer* gbp = ImageReader_getProducer(env, thiz); if (gbp == NULL) { - jniThrowRuntimeException(env, "CpuConsumer is uninitialized"); + jniThrowRuntimeException(env, "Buffer consumer is uninitialized"); return NULL; } @@ -1160,98 +625,115 @@ static jobject ImageReader_getSurface(JNIEnv* env, jobject thiz) return android_view_Surface_createFromIGraphicBufferProducer(env, gbp); } -static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int readerFormat) -{ - int rowStride, pixelStride; - PublicFormat publicReaderFormat = static_cast<PublicFormat>(readerFormat); - int halReaderFormat = android_view_Surface_mapPublicFormatToHalFormat( - publicReaderFormat); - - ALOGV("%s: buffer index: %d", __FUNCTION__, idx); - if (isFormatOpaque(halReaderFormat)) { +static void Image_getLockedImage(JNIEnv* env, jobject thiz, LockedImage *image) { + ALOGV("%s", __FUNCTION__); + BufferItem* buffer = Image_getBufferItem(env, thiz); + if (buffer == NULL) { jniThrowException(env, "java/lang/IllegalStateException", - "Opaque images from Opaque ImageReader do not have any planes"); - return NULL; + "Image is not initialized"); + return; } - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); - - ALOG_ASSERT(buffer != NULL); - if (buffer == NULL) { - jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); + status_t res = lockImageFromBuffer(buffer, + GRALLOC_USAGE_SW_READ_OFTEN, buffer->mFence->dup(), image); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/RuntimeException", + "lock buffer failed for format 0x%x", + buffer->mGraphicBuffer->getPixelFormat()); + return; } - rowStride = Image_imageGetRowStride(env, buffer, idx, halReaderFormat); - pixelStride = Image_imageGetPixelStride(env, buffer, idx, halReaderFormat); + // Carry over some fields from BufferItem. + image->crop = buffer->mCrop; + image->transform = buffer->mTransform; + image->scalingMode = buffer->mScalingMode; + image->timestamp = buffer->mTimestamp; + image->dataSpace = buffer->mDataSpace; + image->frameNumber = buffer->mFrameNumber; + + ALOGV("%s: Successfully locked the image", __FUNCTION__); + // crop, transform, scalingMode, timestamp, and frameNumber should be set by producer, + // and we don't set them here. +} - jobject surfPlaneObj = env->NewObject(gSurfacePlaneClassInfo.clazz, - gSurfacePlaneClassInfo.ctor, thiz, idx, rowStride, pixelStride); +static void Image_getLockedImageInfo(JNIEnv* env, LockedImage* buffer, int idx, + int32_t writerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) { + ALOGV("%s", __FUNCTION__); - return surfPlaneObj; + status_t res = getLockedImageInfo(buffer, idx, writerFormat, base, size, + pixelStride, rowStride); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", + "Pixel format: 0x%x is unsupported", buffer->flexFormat); + } } -static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int readerFormat) +static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, + int numPlanes, int readerFormat) { - uint8_t *base = NULL; - uint32_t size = 0; - jobject byteBuffer; - PublicFormat readerPublicFormat = static_cast<PublicFormat>(readerFormat); - int readerHalFormat = android_view_Surface_mapPublicFormatToHalFormat( - readerPublicFormat); + ALOGV("%s: create SurfacePlane array with size %d", __FUNCTION__, numPlanes); + int rowStride = 0; + int pixelStride = 0; + uint8_t *pData = NULL; + uint32_t dataSize = 0; + jobject byteBuffer = NULL; - ALOGV("%s: buffer index: %d", __FUNCTION__, idx); + PublicFormat publicReaderFormat = static_cast<PublicFormat>(readerFormat); + int halReaderFormat = android_view_Surface_mapPublicFormatToHalFormat( + publicReaderFormat); - if (isFormatOpaque(readerHalFormat)) { - jniThrowException(env, "java/lang/IllegalStateException", - "Opaque images from Opaque ImageReader do not have any plane"); + if (isFormatOpaque(halReaderFormat) && numPlanes > 0) { + String8 msg; + msg.appendFormat("Format 0x%x is opaque, thus not writable, the number of planes (%d)" + " must be 0", halReaderFormat, numPlanes); + jniThrowException(env, "java/lang/IllegalArgumentException", msg.string()); return NULL; } - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); - - if (buffer == NULL) { - jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); - } - - // Create byteBuffer from native buffer - Image_getLockedBufferInfo(env, buffer, idx, &base, &size, readerHalFormat); - - if (size > static_cast<uint32_t>(INT32_MAX)) { - // Byte buffer have 'int capacity', so check the range - jniThrowExceptionFmt(env, "java/lang/IllegalStateException", - "Size too large for bytebuffer capacity %" PRIu32, size); + jobjectArray surfacePlanes = env->NewObjectArray(numPlanes, gSurfacePlaneClassInfo.clazz, + /*initial_element*/NULL); + if (surfacePlanes == NULL) { + jniThrowRuntimeException(env, "Failed to create SurfacePlane arrays," + " probably out of memory"); return NULL; } + if (isFormatOpaque(halReaderFormat)) { + // Return 0 element surface array. + return surfacePlanes; + } + + LockedImage lockedImg = LockedImage(); + Image_getLockedImage(env, thiz, &lockedImg); + // Create all SurfacePlanes + for (int i = 0; i < numPlanes; i++) { + Image_getLockedImageInfo(env, &lockedImg, i, halReaderFormat, + &pData, &dataSize, &pixelStride, &rowStride); + byteBuffer = env->NewDirectByteBuffer(pData, dataSize); + if ((byteBuffer == NULL) && (env->ExceptionCheck() == false)) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to allocate ByteBuffer"); + return NULL; + } - byteBuffer = env->NewDirectByteBuffer(base, size); - // TODO: throw dvm exOutOfMemoryError? - if ((byteBuffer == NULL) && (env->ExceptionCheck() == false)) { - jniThrowException(env, "java/lang/IllegalStateException", "Failed to allocate ByteBuffer"); + // Finally, create this SurfacePlane. + jobject surfacePlane = env->NewObject(gSurfacePlaneClassInfo.clazz, + gSurfacePlaneClassInfo.ctor, thiz, rowStride, pixelStride, byteBuffer); + env->SetObjectArrayElement(surfacePlanes, i, surfacePlane); } - return byteBuffer; + return surfacePlanes; } -static jint Image_getWidth(JNIEnv* env, jobject thiz, jint format) +static jint Image_getWidth(JNIEnv* env, jobject thiz) { - if (isFormatOpaque(format)) { - BufferItem* opaqueBuffer = Image_getOpaqueBuffer(env, thiz); - return Image_getOpaqueBufferWidth(opaqueBuffer); - } else { - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); - return Image_getBufferWidth(buffer); - } + BufferItem* buffer = Image_getBufferItem(env, thiz); + return getBufferWidth(buffer); } -static jint Image_getHeight(JNIEnv* env, jobject thiz, jint format) +static jint Image_getHeight(JNIEnv* env, jobject thiz) { - if (isFormatOpaque(format)) { - BufferItem* opaqueBuffer = Image_getOpaqueBuffer(env, thiz); - return Image_getOpaqueBufferHeight(opaqueBuffer); - } else { - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); - return Image_getBufferHeight(buffer); - } + BufferItem* buffer = Image_getBufferItem(env, thiz); + return getBufferHeight(buffer); } static jint Image_getFormat(JNIEnv* env, jobject thiz, jint readerFormat) @@ -1260,20 +742,21 @@ static jint Image_getFormat(JNIEnv* env, jobject thiz, jint readerFormat) // Assuming opaque reader produce opaque images. return static_cast<jint>(PublicFormat::PRIVATE); } else { - CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); + BufferItem* buffer = Image_getBufferItem(env, thiz); int readerHalFormat = android_view_Surface_mapPublicFormatToHalFormat( static_cast<PublicFormat>(readerFormat)); - int32_t fmt = applyFormatOverrides(buffer->flexFormat, readerHalFormat); + int32_t fmt = applyFormatOverrides( + buffer->mGraphicBuffer->getPixelFormat(), readerHalFormat); // Override the image format to HAL_PIXEL_FORMAT_YCbCr_420_888 if the actual format is // NV21 or YV12. This could only happen when the Gralloc HAL version is v0.1 thus doesn't // support lockycbcr(), the CpuConsumer need to use the lock() method in the // lockNextBuffer() call. For Gralloc HAL v0.2 or newer, this format should already be // overridden to HAL_PIXEL_FORMAT_YCbCr_420_888 for the flexible YUV compatible formats. - if (fmt == HAL_PIXEL_FORMAT_YCrCb_420_SP || fmt == HAL_PIXEL_FORMAT_YV12) { + if (isPossiblyYUV(fmt)) { fmt = HAL_PIXEL_FORMAT_YCbCr_420_888; } PublicFormat publicFmt = android_view_Surface_mapHalFormatDataspaceToPublicFormat( - fmt, buffer->dataSpace); + fmt, buffer->mDataSpace); return static_cast<jint>(publicFmt); } } @@ -1293,11 +776,10 @@ static const JNINativeMethod gImageReaderMethods[] = { }; static const JNINativeMethod gImageMethods[] = { - {"nativeImageGetBuffer", "(II)Ljava/nio/ByteBuffer;", (void*)Image_getByteBuffer }, - {"nativeCreatePlane", "(II)Landroid/media/ImageReader$SurfaceImage$SurfacePlane;", - (void*)Image_createSurfacePlane }, - {"nativeGetWidth", "(I)I", (void*)Image_getWidth }, - {"nativeGetHeight", "(I)I", (void*)Image_getHeight }, + {"nativeCreatePlanes", "(II)[Landroid/media/ImageReader$SurfaceImage$SurfacePlane;", + (void*)Image_createSurfacePlanes }, + {"nativeGetWidth", "()I", (void*)Image_getWidth }, + {"nativeGetHeight", "()I", (void*)Image_getHeight }, {"nativeGetFormat", "(I)I", (void*)Image_getFormat }, }; diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp index f50da8547af2..d5d9fc94bf29 100644 --- a/media/jni/android_media_ImageWriter.cpp +++ b/media/jni/android_media_ImageWriter.cpp @@ -16,34 +16,28 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "ImageWriter_JNI" +#include "android_media_Utils.h" + #include <utils/Log.h> #include <utils/String8.h> #include <gui/IProducerListener.h> #include <gui/Surface.h> -#include <gui/CpuConsumer.h> #include <android_runtime/AndroidRuntime.h> #include <android_runtime/android_view_Surface.h> #include <camera3.h> - #include <jni.h> #include <JNIHelp.h> #include <stdint.h> #include <inttypes.h> -#define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) - #define IMAGE_BUFFER_JNI_ID "mNativeBuffer" // ---------------------------------------------------------------------------- using namespace android; -enum { - IMAGE_WRITER_MAX_NUM_PLANES = 3, -}; - static struct { jmethodID postEventFromNative; jfieldID mWriterFormat; @@ -60,8 +54,6 @@ static struct { jmethodID ctor; } gSurfacePlaneClassInfo; -typedef CpuConsumer::LockedBuffer LockedImage; - // ---------------------------------------------------------------------------- class JNIImageWriterContext : public BnProducerListener { @@ -181,13 +173,11 @@ extern "C" { // -------------------------------Private method declarations-------------- -static bool isPossiblyYUV(PixelFormat format); static void Image_setNativeContext(JNIEnv* env, jobject thiz, sp<GraphicBuffer> buffer, int fenceFd); static void Image_getNativeContext(JNIEnv* env, jobject thiz, GraphicBuffer** buffer, int* fenceFd); static void Image_unlockIfLocked(JNIEnv* env, jobject thiz); -static bool isFormatOpaque(int format); // --------------------------ImageWriter methods--------------------------------------- @@ -672,28 +662,6 @@ static jint Image_getHeight(JNIEnv* env, jobject thiz) { return buffer->getHeight(); } -// Some formats like JPEG defined with different values between android.graphics.ImageFormat and -// graphics.h, need convert to the one defined in graphics.h here. -static int Image_getPixelFormat(JNIEnv* env, int format) { - int jpegFormat; - jfieldID fid; - - ALOGV("%s: format = 0x%x", __FUNCTION__, format); - - jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat"); - ALOG_ASSERT(imageFormatClazz != NULL); - - fid = env->GetStaticFieldID(imageFormatClazz, "JPEG", "I"); - jpegFormat = env->GetStaticIntField(imageFormatClazz, fid); - - // Translate the JPEG to BLOB for camera purpose. - if (format == jpegFormat) { - format = HAL_PIXEL_FORMAT_BLOB; - } - - return format; -} - static jint Image_getFormat(JNIEnv* env, jobject thiz) { ALOGV("%s", __FUNCTION__); GraphicBuffer* buffer; @@ -704,7 +672,10 @@ static jint Image_getFormat(JNIEnv* env, jobject thiz) { return 0; } - return Image_getPixelFormat(env, buffer->getPixelFormat()); + // ImageWriter doesn't support data space yet, assuming it is unknown. + PublicFormat publicFmt = android_view_Surface_mapHalFormatDataspaceToPublicFormat( + buffer->getPixelFormat(), HAL_DATASPACE_UNKNOWN); + return static_cast<jint>(publicFmt); } static void Image_setFenceFd(JNIEnv* env, jobject thiz, int fenceFd) { @@ -723,272 +694,34 @@ static void Image_getLockedImage(JNIEnv* env, jobject thiz, LockedImage *image) return; } - void* pData = NULL; - android_ycbcr ycbcr = android_ycbcr(); - status_t res; - int format = Image_getFormat(env, thiz); - int flexFormat = format; - if (isPossiblyYUV(format)) { - // ImageWriter doesn't use crop by itself, app sets it, use the no crop version. - res = buffer->lockAsyncYCbCr(GRALLOC_USAGE_SW_WRITE_OFTEN, &ycbcr, fenceFd); - // Clear the fenceFd as it is already consumed by lock call. - Image_setFenceFd(env, thiz, /*fenceFd*/-1); - if (res != OK) { - jniThrowRuntimeException(env, "lockAsyncYCbCr failed for YUV buffer"); - return; - } - pData = ycbcr.y; - flexFormat = HAL_PIXEL_FORMAT_YCbCr_420_888; - } - - // lockAsyncYCbCr for YUV is unsuccessful. - if (pData == NULL) { - res = buffer->lockAsync(GRALLOC_USAGE_SW_WRITE_OFTEN, &pData, fenceFd); - if (res != OK) { - jniThrowRuntimeException(env, "lockAsync failed"); - return; - } + // ImageWriter doesn't use crop by itself, app sets it, use the no crop version. + const Rect noCrop(buffer->width, buffer->height); + status_t res = lockImageFromBuffer( + buffer, GRALLOC_USAGE_SW_WRITE_OFTEN, noCrop, fenceFd, image); + // Clear the fenceFd as it is already consumed by lock call. + Image_setFenceFd(env, thiz, /*fenceFd*/-1); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/RuntimeException", + "lock buffer failed for format 0x%x", + buffer->getPixelFormat()); + return; } - image->data = reinterpret_cast<uint8_t*>(pData); - image->width = buffer->getWidth(); - image->height = buffer->getHeight(); - image->format = format; - image->flexFormat = flexFormat; - image->stride = (ycbcr.y != NULL) ? static_cast<uint32_t>(ycbcr.ystride) : buffer->getStride(); - - image->dataCb = reinterpret_cast<uint8_t*>(ycbcr.cb); - image->dataCr = reinterpret_cast<uint8_t*>(ycbcr.cr); - image->chromaStride = static_cast<uint32_t>(ycbcr.cstride); - image->chromaStep = static_cast<uint32_t>(ycbcr.chroma_step); - ALOGV("Successfully locked the image"); + ALOGV("%s: Successfully locked the image", __FUNCTION__); // crop, transform, scalingMode, timestamp, and frameNumber should be set by producer, // and we don't set them here. } -static bool usingRGBAToJpegOverride(int32_t bufferFormat, int32_t writerCtxFormat) { - return writerCtxFormat == HAL_PIXEL_FORMAT_BLOB && bufferFormat == HAL_PIXEL_FORMAT_RGBA_8888; -} - -static int32_t applyFormatOverrides(int32_t bufferFormat, int32_t writerCtxFormat) -{ - // Using HAL_PIXEL_FORMAT_RGBA_8888 gralloc buffers containing JPEGs to get around SW - // write limitations for some platforms (b/17379185). - if (usingRGBAToJpegOverride(bufferFormat, writerCtxFormat)) { - return HAL_PIXEL_FORMAT_BLOB; - } - return bufferFormat; -} - -static uint32_t Image_getJpegSize(LockedImage* buffer, bool usingRGBAOverride) { - ALOGV("%s", __FUNCTION__); - ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); - uint32_t size = 0; - uint32_t width = buffer->width; - uint8_t* jpegBuffer = buffer->data; - - if (usingRGBAOverride) { - width = (buffer->width + buffer->stride * (buffer->height - 1)) * 4; - } - - // First check for JPEG transport header at the end of the buffer - uint8_t* header = jpegBuffer + (width - sizeof(struct camera3_jpeg_blob)); - struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header); - if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID) { - size = blob->jpeg_size; - ALOGV("%s: Jpeg size = %d", __FUNCTION__, size); - } - - // failed to find size, default to whole buffer - if (size == 0) { - /* - * This is a problem because not including the JPEG header - * means that in certain rare situations a regular JPEG blob - * will be misidentified as having a header, in which case - * we will get a garbage size value. - */ - ALOGW("%s: No JPEG header detected, defaulting to size=width=%d", - __FUNCTION__, width); - size = width; - } - - return size; -} - static void Image_getLockedImageInfo(JNIEnv* env, LockedImage* buffer, int idx, int32_t writerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) { ALOGV("%s", __FUNCTION__); - ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); - ALOG_ASSERT(base != NULL, "base is NULL!!!"); - ALOG_ASSERT(size != NULL, "size is NULL!!!"); - ALOG_ASSERT(pixelStride != NULL, "pixelStride is NULL!!!"); - ALOG_ASSERT(rowStride != NULL, "rowStride is NULL!!!"); - ALOG_ASSERT((idx < IMAGE_WRITER_MAX_NUM_PLANES) && (idx >= 0)); - - ALOGV("%s: buffer: %p", __FUNCTION__, buffer); - - uint32_t dataSize, ySize, cSize, cStride; - uint32_t pStride = 0, rStride = 0; - uint8_t *cb, *cr; - uint8_t *pData = NULL; - int bytesPerPixel = 0; - - dataSize = ySize = cSize = cStride = 0; - int32_t fmt = buffer->flexFormat; - - bool usingRGBAOverride = usingRGBAToJpegOverride(fmt, writerFormat); - fmt = applyFormatOverrides(fmt, writerFormat); - switch (fmt) { - case HAL_PIXEL_FORMAT_YCbCr_420_888: - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - buffer->dataCb : - buffer->dataCr; - // only map until last pixel - if (idx == 0) { - pStride = 1; - rStride = buffer->stride; - dataSize = buffer->stride * (buffer->height - 1) + buffer->width; - } else { - pStride = buffer->chromaStep; - rStride = buffer->chromaStride; - dataSize = buffer->chromaStride * (buffer->height / 2 - 1) + - buffer->chromaStep * (buffer->width / 2 - 1) + 1; - } - break; - // NV21 - case HAL_PIXEL_FORMAT_YCrCb_420_SP: - cr = buffer->data + (buffer->stride * buffer->height); - cb = cr + 1; - // only map until last pixel - ySize = buffer->width * (buffer->height - 1) + buffer->width; - cSize = buffer->width * (buffer->height / 2 - 1) + buffer->width - 1; - - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - cb: - cr; - - dataSize = (idx == 0) ? ySize : cSize; - pStride = (idx == 0) ? 1 : 2; - rStride = buffer->width; - break; - case HAL_PIXEL_FORMAT_YV12: - // Y and C stride need to be 16 pixel aligned. - LOG_ALWAYS_FATAL_IF(buffer->stride % 16, - "Stride is not 16 pixel aligned %d", buffer->stride); - - ySize = buffer->stride * buffer->height; - cStride = ALIGN(buffer->stride / 2, 16); - cr = buffer->data + ySize; - cSize = cStride * buffer->height / 2; - cb = cr + cSize; - - pData = - (idx == 0) ? - buffer->data : - (idx == 1) ? - cb : - cr; - dataSize = (idx == 0) ? ySize : cSize; - pStride = 1; - rStride = (idx == 0) ? buffer->stride : ALIGN(buffer->stride / 2, 16); - break; - case HAL_PIXEL_FORMAT_Y8: - // Single plane, 8bpp. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - - pData = buffer->data; - dataSize = buffer->stride * buffer->height; - pStride = 1; - rStride = buffer->stride; - break; - case HAL_PIXEL_FORMAT_Y16: - bytesPerPixel = 2; - // Single plane, 16bpp, strides are specified in pixels, not in bytes - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - pStride = bytesPerPixel; - rStride = buffer->stride * 2; - break; - case HAL_PIXEL_FORMAT_BLOB: - // Used for JPEG data, height must be 1, width == size, single plane. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - ALOG_ASSERT(buffer->height == 1, "JPEG should has height value %d", buffer->height); - - pData = buffer->data; - dataSize = Image_getJpegSize(buffer, usingRGBAOverride); - pStride = bytesPerPixel; - rowStride = 0; - break; - case HAL_PIXEL_FORMAT_RAW16: - // Single plane 16bpp bayer data. - bytesPerPixel = 2; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - pStride = bytesPerPixel; - rStride = buffer->stride * 2; - break; - case HAL_PIXEL_FORMAT_RAW10: - // Single plane 10bpp bayer data. - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - LOG_ALWAYS_FATAL_IF(buffer->width % 4, - "Width is not multiple of 4 %d", buffer->width); - LOG_ALWAYS_FATAL_IF(buffer->height % 2, - "Height is not even %d", buffer->height); - LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 10 / 8), - "stride (%d) should be at least %d", - buffer->stride, buffer->width * 10 / 8); - pData = buffer->data; - dataSize = buffer->stride * buffer->height; - pStride = 0; - rStride = buffer->stride; - break; - case HAL_PIXEL_FORMAT_RGBA_8888: - case HAL_PIXEL_FORMAT_RGBX_8888: - // Single plane, 32bpp. - bytesPerPixel = 4; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - pStride = bytesPerPixel; - rStride = buffer->stride * 4; - break; - case HAL_PIXEL_FORMAT_RGB_565: - // Single plane, 16bpp. - bytesPerPixel = 2; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - pStride = bytesPerPixel; - rStride = buffer->stride * 2; - break; - case HAL_PIXEL_FORMAT_RGB_888: - // Single plane, 24bpp. - bytesPerPixel = 3; - ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); - pData = buffer->data; - dataSize = buffer->stride * buffer->height * bytesPerPixel; - pStride = bytesPerPixel; - rStride = buffer->stride * 3; - break; - default: - jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", - "Pixel format: 0x%x is unsupported", fmt); - break; - } - *base = pData; - *size = dataSize; - *pixelStride = pStride; - *rowStride = rStride; + status_t res = getLockedImageInfo(buffer, idx, writerFormat, base, size, + pixelStride, rowStride); + if (res != OK) { + jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", + "Pixel format: 0x%x is unsupported", buffer->flexFormat); + } } static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, @@ -1024,7 +757,8 @@ static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, Image_getLockedImage(env, thiz, &lockedImg); // Create all SurfacePlanes - writerFormat = Image_getPixelFormat(env, writerFormat); + PublicFormat publicWriterFormat = static_cast<PublicFormat>(writerFormat); + writerFormat = android_view_Surface_mapPublicFormatToHalFormat(publicWriterFormat); for (int i = 0; i < numPlanes; i++) { Image_getLockedImageInfo(env, &lockedImg, i, writerFormat, &pData, &dataSize, &pixelStride, &rowStride); @@ -1044,39 +778,6 @@ static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, return surfacePlanes; } -// -------------------------------Private convenience methods-------------------- - -static bool isFormatOpaque(int format) { - // Only treat IMPLEMENTATION_DEFINED as an opaque format for now. - return format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED; -} - -static bool isPossiblyYUV(PixelFormat format) { - switch (static_cast<int>(format)) { - case HAL_PIXEL_FORMAT_RGBA_8888: - case HAL_PIXEL_FORMAT_RGBX_8888: - case HAL_PIXEL_FORMAT_RGB_888: - case HAL_PIXEL_FORMAT_RGB_565: - case HAL_PIXEL_FORMAT_BGRA_8888: - case HAL_PIXEL_FORMAT_Y8: - case HAL_PIXEL_FORMAT_Y16: - case HAL_PIXEL_FORMAT_RAW16: - case HAL_PIXEL_FORMAT_RAW10: - case HAL_PIXEL_FORMAT_RAW_OPAQUE: - case HAL_PIXEL_FORMAT_BLOB: - case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: - return false; - - case HAL_PIXEL_FORMAT_YV12: - case HAL_PIXEL_FORMAT_YCbCr_420_888: - case HAL_PIXEL_FORMAT_YCbCr_422_SP: - case HAL_PIXEL_FORMAT_YCrCb_420_SP: - case HAL_PIXEL_FORMAT_YCbCr_422_I: - default: - return true; - } -} - } // extern "C" // ---------------------------------------------------------------------------- diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp index 9c4f7c4b2aff..f4296617d734 100644 --- a/media/jni/android_media_Utils.cpp +++ b/media/jni/android_media_Utils.cpp @@ -26,6 +26,8 @@ #include <nativehelper/ScopedLocalRef.h> +#define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) + namespace android { FileStream::FileStream(const int fd) @@ -509,5 +511,391 @@ status_t ConvertKeyValueArraysToMessage( return OK; } +// -----------Utility functions used by ImageReader/Writer JNI----------------- + +enum { + IMAGE_MAX_NUM_PLANES = 3, +}; + +bool usingRGBAToJpegOverride(int32_t imageFormat, + int32_t containerFormat) { + return containerFormat == HAL_PIXEL_FORMAT_BLOB && imageFormat == HAL_PIXEL_FORMAT_RGBA_8888; +} + +int32_t applyFormatOverrides(int32_t imageFormat, int32_t containerFormat) { + // Using HAL_PIXEL_FORMAT_RGBA_8888 gralloc buffers containing JPEGs to get around SW + // write limitations for some platforms (b/17379185). + if (usingRGBAToJpegOverride(imageFormat, containerFormat)) { + return HAL_PIXEL_FORMAT_BLOB; + } + return containerFormat; +} + +bool isFormatOpaque(int format) { + // This is the only opaque format exposed in the ImageFormat public API. + // Note that we do support CPU access for HAL_PIXEL_FORMAT_RAW_OPAQUE + // (ImageFormat#RAW_PRIVATE) so it doesn't count as opaque here. + return format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED; +} + +bool isPossiblyYUV(PixelFormat format) { + switch (static_cast<int>(format)) { + case HAL_PIXEL_FORMAT_RGBA_8888: + case HAL_PIXEL_FORMAT_RGBX_8888: + case HAL_PIXEL_FORMAT_RGB_888: + case HAL_PIXEL_FORMAT_RGB_565: + case HAL_PIXEL_FORMAT_BGRA_8888: + case HAL_PIXEL_FORMAT_Y8: + case HAL_PIXEL_FORMAT_Y16: + case HAL_PIXEL_FORMAT_RAW16: + case HAL_PIXEL_FORMAT_RAW10: + case HAL_PIXEL_FORMAT_RAW_OPAQUE: + case HAL_PIXEL_FORMAT_BLOB: + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + return false; + + case HAL_PIXEL_FORMAT_YV12: + case HAL_PIXEL_FORMAT_YCbCr_420_888: + case HAL_PIXEL_FORMAT_YCrCb_420_SP: + default: + return true; + } +} + +uint32_t Image_getJpegSize(LockedImage* buffer, bool usingRGBAOverride) { + ALOGV("%s", __FUNCTION__); + LOG_ALWAYS_FATAL_IF(buffer == NULL, "Input buffer is NULL!!!"); + uint32_t size = 0; + uint32_t width = buffer->width; + uint8_t* jpegBuffer = buffer->data; + + if (usingRGBAOverride) { + width = (buffer->width + buffer->stride * (buffer->height - 1)) * 4; + } + + // First check for JPEG transport header at the end of the buffer + uint8_t* header = jpegBuffer + (width - sizeof(struct camera3_jpeg_blob)); + struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header); + if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID) { + size = blob->jpeg_size; + ALOGV("%s: Jpeg size = %d", __FUNCTION__, size); + } + + // failed to find size, default to whole buffer + if (size == 0) { + /* + * This is a problem because not including the JPEG header + * means that in certain rare situations a regular JPEG blob + * will be mis-identified as having a header, in which case + * we will get a garbage size value. + */ + ALOGW("%s: No JPEG header detected, defaulting to size=width=%d", + __FUNCTION__, width); + size = width; + } + + return size; +} + +status_t getLockedImageInfo(LockedImage* buffer, int idx, + int32_t containerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) { + ALOGV("%s", __FUNCTION__); + LOG_ALWAYS_FATAL_IF(buffer == NULL, "Input buffer is NULL!!!"); + LOG_ALWAYS_FATAL_IF(base == NULL, "base is NULL!!!"); + LOG_ALWAYS_FATAL_IF(size == NULL, "size is NULL!!!"); + LOG_ALWAYS_FATAL_IF(pixelStride == NULL, "pixelStride is NULL!!!"); + LOG_ALWAYS_FATAL_IF(rowStride == NULL, "rowStride is NULL!!!"); + LOG_ALWAYS_FATAL_IF((idx >= IMAGE_MAX_NUM_PLANES) || (idx < 0), "idx (%d) is illegal", idx); + + ALOGV("%s: buffer: %p", __FUNCTION__, buffer); + + uint32_t dataSize, ySize, cSize, cStride; + uint32_t pStride = 0, rStride = 0; + uint8_t *cb, *cr; + uint8_t *pData = NULL; + int bytesPerPixel = 0; + + dataSize = ySize = cSize = cStride = 0; + int32_t fmt = buffer->flexFormat; + + bool usingRGBAOverride = usingRGBAToJpegOverride(fmt, containerFormat); + fmt = applyFormatOverrides(fmt, containerFormat); + switch (fmt) { + case HAL_PIXEL_FORMAT_YCbCr_420_888: + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + buffer->dataCb : + buffer->dataCr; + // only map until last pixel + if (idx == 0) { + pStride = 1; + rStride = buffer->stride; + dataSize = buffer->stride * (buffer->height - 1) + buffer->width; + } else { + pStride = buffer->chromaStep; + rStride = buffer->chromaStride; + dataSize = buffer->chromaStride * (buffer->height / 2 - 1) + + buffer->chromaStep * (buffer->width / 2 - 1) + 1; + } + break; + // NV21 + case HAL_PIXEL_FORMAT_YCrCb_420_SP: + cr = buffer->data + (buffer->stride * buffer->height); + cb = cr + 1; + // only map until last pixel + ySize = buffer->width * (buffer->height - 1) + buffer->width; + cSize = buffer->width * (buffer->height / 2 - 1) + buffer->width - 1; + + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + cb: + cr; + + dataSize = (idx == 0) ? ySize : cSize; + pStride = (idx == 0) ? 1 : 2; + rStride = buffer->width; + break; + case HAL_PIXEL_FORMAT_YV12: + // Y and C stride need to be 16 pixel aligned. + LOG_ALWAYS_FATAL_IF(buffer->stride % 16, + "Stride is not 16 pixel aligned %d", buffer->stride); + + ySize = buffer->stride * buffer->height; + cStride = ALIGN(buffer->stride / 2, 16); + cr = buffer->data + ySize; + cSize = cStride * buffer->height / 2; + cb = cr + cSize; + + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + cb : + cr; + dataSize = (idx == 0) ? ySize : cSize; + pStride = 1; + rStride = (idx == 0) ? buffer->stride : ALIGN(buffer->stride / 2, 16); + break; + case HAL_PIXEL_FORMAT_Y8: + // Single plane, 8bpp. + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + + pData = buffer->data; + dataSize = buffer->stride * buffer->height; + pStride = 1; + rStride = buffer->stride; + break; + case HAL_PIXEL_FORMAT_Y16: + bytesPerPixel = 2; + // Single plane, 16bpp, strides are specified in pixels, not in bytes + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_BLOB: + // Used for JPEG data, height must be 1, width == size, single plane. + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + LOG_ALWAYS_FATAL_IF(buffer->height != 1, + "BLOB format buffer should has height value %d", buffer->height); + + pData = buffer->data; + dataSize = Image_getJpegSize(buffer, usingRGBAOverride); + pStride = 0; + rStride = 0; + break; + case HAL_PIXEL_FORMAT_RAW16: + // Single plane 16bpp bayer data. + bytesPerPixel = 2; + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_RAW_OPAQUE: + // Used for RAW_OPAQUE data, height must be 1, width == size, single plane. + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + LOG_ALWAYS_FATAL_IF(buffer->height != 1, + "RAW_PRIVATE should has height value one but got %d", buffer->height); + pData = buffer->data; + dataSize = buffer->width; + pStride = 0; // RAW OPAQUE doesn't have pixel stride + rStride = 0; // RAW OPAQUE doesn't have row stride + break; + case HAL_PIXEL_FORMAT_RAW10: + // Single plane 10bpp bayer data. + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + LOG_ALWAYS_FATAL_IF(buffer->width % 4, + "Width is not multiple of 4 %d", buffer->width); + LOG_ALWAYS_FATAL_IF(buffer->height % 2, + "Height is not even %d", buffer->height); + LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 10 / 8), + "stride (%d) should be at least %d", + buffer->stride, buffer->width * 10 / 8); + pData = buffer->data; + dataSize = buffer->stride * buffer->height; + pStride = 0; + rStride = buffer->stride; + break; + case HAL_PIXEL_FORMAT_RAW12: + // Single plane 10bpp bayer data. + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + LOG_ALWAYS_FATAL_IF(buffer->width % 4, + "Width is not multiple of 4 %d", buffer->width); + LOG_ALWAYS_FATAL_IF(buffer->height % 2, + "Height is not even %d", buffer->height); + LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 12 / 8), + "stride (%d) should be at least %d", + buffer->stride, buffer->width * 12 / 8); + pData = buffer->data; + dataSize = buffer->stride * buffer->height; + pStride = 0; + rStride = buffer->stride; + break; + case HAL_PIXEL_FORMAT_RGBA_8888: + case HAL_PIXEL_FORMAT_RGBX_8888: + // Single plane, 32bpp. + bytesPerPixel = 4; + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 4; + break; + case HAL_PIXEL_FORMAT_RGB_565: + // Single plane, 16bpp. + bytesPerPixel = 2; + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_RGB_888: + // Single plane, 24bpp. + bytesPerPixel = 3; + LOG_ALWAYS_FATAL_IF(idx != 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 3; + break; + default: + return BAD_VALUE; + } + + *base = pData; + *size = dataSize; + *pixelStride = pStride; + *rowStride = rStride; + + return OK; +} + +status_t lockImageFromBuffer(sp<GraphicBuffer> buffer, uint32_t inUsage, + const Rect& rect, int fenceFd, LockedImage* outputImage) { + ALOGV("%s: Try to lock the GraphicBuffer", __FUNCTION__); + + if (buffer == nullptr || outputImage == nullptr) { + ALOGE("Input BufferItem or output LockedImage is NULL!"); + return BAD_VALUE; + } + if (isFormatOpaque(buffer->getPixelFormat())) { + ALOGE("Opaque format buffer is not lockable!"); + return BAD_VALUE; + } + + void* pData = NULL; + android_ycbcr ycbcr = android_ycbcr(); + status_t res; + int format = buffer->getPixelFormat(); + int flexFormat = format; + if (isPossiblyYUV(format)) { + res = buffer->lockAsyncYCbCr(inUsage, rect, &ycbcr, fenceFd); + pData = ycbcr.y; + flexFormat = HAL_PIXEL_FORMAT_YCbCr_420_888; + } + + // lockAsyncYCbCr for YUV is unsuccessful. + if (pData == NULL) { + res = buffer->lockAsync(inUsage, rect, &pData, fenceFd); + if (res != OK) { + ALOGE("Lock buffer failed!"); + return res; + } + } + + outputImage->data = reinterpret_cast<uint8_t*>(pData); + outputImage->width = buffer->getWidth(); + outputImage->height = buffer->getHeight(); + outputImage->format = format; + outputImage->flexFormat = flexFormat; + outputImage->stride = + (ycbcr.y != NULL) ? static_cast<uint32_t>(ycbcr.ystride) : buffer->getStride(); + + outputImage->dataCb = reinterpret_cast<uint8_t*>(ycbcr.cb); + outputImage->dataCr = reinterpret_cast<uint8_t*>(ycbcr.cr); + outputImage->chromaStride = static_cast<uint32_t>(ycbcr.cstride); + outputImage->chromaStep = static_cast<uint32_t>(ycbcr.chroma_step); + ALOGV("%s: Successfully locked the image from the GraphicBuffer", __FUNCTION__); + // Crop, transform, scalingMode, timestamp, and frameNumber should be set by caller, + // and cann't be set them here. + return OK; +} + +status_t lockImageFromBuffer(BufferItem* bufferItem, uint32_t inUsage, + int fenceFd, LockedImage* outputImage) { + ALOGV("%s: Try to lock the BufferItem", __FUNCTION__); + if (bufferItem == nullptr || outputImage == nullptr) { + ALOGE("Input BufferItem or output LockedImage is NULL!"); + return BAD_VALUE; + } + + status_t res = lockImageFromBuffer(bufferItem->mGraphicBuffer, inUsage, bufferItem->mCrop, + fenceFd, outputImage); + if (res != OK) { + ALOGE("%s: lock graphic buffer failed", __FUNCTION__); + return res; + } + + outputImage->crop = bufferItem->mCrop; + outputImage->transform = bufferItem->mTransform; + outputImage->scalingMode = bufferItem->mScalingMode; + outputImage->timestamp = bufferItem->mTimestamp; + outputImage->dataSpace = bufferItem->mDataSpace; + outputImage->frameNumber = bufferItem->mFrameNumber; + ALOGV("%s: Successfully locked the image from the BufferItem", __FUNCTION__); + return OK; +} + +int getBufferWidth(BufferItem* buffer) { + if (buffer == NULL) return -1; + + if (!buffer->mCrop.isEmpty()) { + return buffer->mCrop.getWidth(); + } + + ALOGV("%s: buffer->mGraphicBuffer: %p", __FUNCTION__, buffer->mGraphicBuffer.get()); + return buffer->mGraphicBuffer->getWidth(); +} + +int getBufferHeight(BufferItem* buffer) { + if (buffer == NULL) return -1; + + if (!buffer->mCrop.isEmpty()) { + return buffer->mCrop.getHeight(); + } + + ALOGV("%s: buffer->mGraphicBuffer: %p", __FUNCTION__, buffer->mGraphicBuffer.get()); + return buffer->mGraphicBuffer->getHeight(); +} + } // namespace android diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h index a30e1be586f9..3791ec933cd9 100644 --- a/media/jni/android_media_Utils.h +++ b/media/jni/android_media_Utils.h @@ -25,6 +25,8 @@ #include <JNIHelp.h> #include <utils/KeyedVector.h> #include <utils/String8.h> +#include <gui/CpuConsumer.h> +#include <camera3.h> namespace android { @@ -69,6 +71,33 @@ status_t ConvertKeyValueArraysToMessage( JNIEnv *env, jobjectArray keys, jobjectArray values, sp<AMessage> *msg); +// -----------Utility functions used by ImageReader/Writer JNI----------------- + +typedef CpuConsumer::LockedBuffer LockedImage; + +bool usingRGBAToJpegOverride(int32_t imageFormat, int32_t containerFormat); + +int32_t applyFormatOverrides(int32_t imageFormat, int32_t containerFormat); + +uint32_t Image_getJpegSize(LockedImage* buffer, bool usingRGBAOverride); + +bool isFormatOpaque(int format); + +bool isPossiblyYUV(PixelFormat format); + +status_t getLockedImageInfo(LockedImage* buffer, int idx, int32_t containerFormat, + uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride); + +status_t lockImageFromBuffer(sp<GraphicBuffer> buffer, uint32_t inUsage, + const Rect& rect, int fenceFd, LockedImage* outputImage); + +status_t lockImageFromBuffer(BufferItem* bufferItem, uint32_t inUsage, + int fenceFd, LockedImage* outputImage); + +int getBufferWidth(BufferItem *buffer); + +int getBufferHeight(BufferItem *buffer); + }; // namespace android #endif // _ANDROID_MEDIA_UTILS_H_ diff --git a/media/tests/MediaFrameworkTest/Android.mk b/media/tests/MediaFrameworkTest/Android.mk index 7c1142bfffc4..7e438a12f31b 100644 --- a/media/tests/MediaFrameworkTest/Android.mk +++ b/media/tests/MediaFrameworkTest/Android.mk @@ -11,7 +11,6 @@ LOCAL_JAVA_LANGUAGE_VERSION := 1.8 LOCAL_STATIC_JAVA_LIBRARIES := easymocklib \ mockito-target \ - core-tests \ android-support-test \ android-ex-camera2 diff --git a/packages/DocumentsUI/res/layout/dialog_delete_confirmation.xml b/packages/DocumentsUI/res/layout/dialog_delete_confirmation.xml index 990ce0b3352b..a1c291093f41 100644 --- a/packages/DocumentsUI/res/layout/dialog_delete_confirmation.xml +++ b/packages/DocumentsUI/res/layout/dialog_delete_confirmation.xml @@ -18,8 +18,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="30dp" - android:gravity="center" + android:paddingTop="24dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" android:textAppearance="@android:style/TextAppearance.Material.Subhead" android:textColor="@*android:color/primary_text_default_material_light"> </TextView> diff --git a/packages/DocumentsUI/res/menu/activity.xml b/packages/DocumentsUI/res/menu/activity.xml index 79f6fa994232..2aee569e8086 100644 --- a/packages/DocumentsUI/res/menu/activity.xml +++ b/packages/DocumentsUI/res/menu/activity.xml @@ -66,7 +66,7 @@ android:id="@+id/menu_sort" android:title="@string/menu_sort" android:icon="@drawable/ic_menu_sortby" - android:showAsAction="never"> + android:showAsAction="ifRoom"> <menu> <item android:id="@+id/menu_sort_name" diff --git a/packages/DocumentsUI/res/values-sw720dp-land/dimens.xml b/packages/DocumentsUI/res/values-sw720dp-land/dimens.xml index fa11244141f7..1b67ee50ca7d 100644 --- a/packages/DocumentsUI/res/values-sw720dp-land/dimens.xml +++ b/packages/DocumentsUI/res/values-sw720dp-land/dimens.xml @@ -20,4 +20,5 @@ <dimen name="list_divider_inset">80dp</dimen> + <dimen name="max_drawer_width">320dp</dimen> </resources> diff --git a/packages/DocumentsUI/res/values-sw720dp/dimens.xml b/packages/DocumentsUI/res/values-sw720dp/dimens.xml index b5d1150eda63..982b204c6c51 100644 --- a/packages/DocumentsUI/res/values-sw720dp/dimens.xml +++ b/packages/DocumentsUI/res/values-sw720dp/dimens.xml @@ -20,4 +20,5 @@ <dimen name="list_item_padding">24dp</dimen> + <dimen name="max_drawer_width">320dp</dimen> </resources> diff --git a/packages/DocumentsUI/res/values/dimens.xml b/packages/DocumentsUI/res/values/dimens.xml index 9fc8a73874da..5af7da33d997 100644 --- a/packages/DocumentsUI/res/values/dimens.xml +++ b/packages/DocumentsUI/res/values/dimens.xml @@ -37,4 +37,5 @@ <dimen name="dir_elevation">8dp</dimen> <dimen name="drag_shadow_size">120dp</dimen> <dimen name="grid_item_elevation">2dp</dimen> + <dimen name="max_drawer_width">280dp</dimen> </resources> diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index 6e1b30e97b30..e2d1870cb122 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -213,4 +213,9 @@ <item quantity="one">Delete <xliff:g id="count" example="1">%1$d</xliff:g> file?</item> <item quantity="other">Delete <xliff:g id="count" example="3">%1$d</xliff:g> files?</item> </plurals> + <!-- Label text showing user how many items are selected. Can be one or more elements. --> + <plurals name="elements_selected"> + <item quantity="one"><xliff:g id="count" example="1">%1$d</xliff:g> selected</item> + <item quantity="other"><xliff:g id="count" example="3">%1$d</xliff:g> selected</item> + </plurals> </resources> diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java index 6efe9d17d203..c9d18b3791e6 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java @@ -154,7 +154,7 @@ public abstract class BaseActivity extends Activity final MenuItem fileSize = menu.findItem(R.id.menu_file_size); // Search uses backend ranking; no sorting, recents doesn't support sort. - sort.setVisible(!inRecents && !mSearchManager.isSearching()); + sort.setEnabled(!inRecents && !mSearchManager.isSearching()); sortSize.setVisible(mState.showSize); // Only sort by size when file sizes are visible fileSize.setVisible(!mState.forceSize); @@ -244,6 +244,7 @@ public abstract class BaseActivity extends Activity return true; case R.id.menu_search: + // SearchViewManager listens for this directly. return false; case R.id.menu_sort_name: @@ -366,6 +367,7 @@ public abstract class BaseActivity extends Activity assert(canSearchRoot()); reloadSearch(query); + invalidateOptionsMenu(); } private void reloadSearch(String query) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/Display.java b/packages/DocumentsUI/src/com/android/documentsui/Display.java new file mode 100644 index 000000000000..bae2d58c7abb --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/Display.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Point; +import android.util.TypedValue; + +/* + * Convenience class for getting display related attributes + */ +public final class Display { + /* + * Returns the screen width in pixels. + */ + public static float screenWidth(Activity activity) { + Point size = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(size); + return size.x; + } + + /* + * Returns logical density of the display. + */ + public static float density(Context context) { + return context.getResources().getDisplayMetrics().density; + } + + /* + * Returns action bar height in pixels. + */ + public static float actionBarHeight(Context context) { + int actionBarHeight = 0; + TypedValue tv = new TypedValue(); + if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, + context.getResources().getDisplayMetrics()); + } + return actionBarHeight; + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java b/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java index 9fceff569d76..020f2c0ad875 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java @@ -16,10 +16,14 @@ package com.android.documentsui; +import static com.android.documentsui.Shared.DEBUG; + import android.app.Activity; +import android.content.Context; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout.DrawerListener; +import android.util.Log; import android.view.View; import android.widget.Toolbar; @@ -30,6 +34,8 @@ import android.widget.Toolbar; */ abstract class DrawerController implements DrawerListener { + public static final String TAG = "DrawerController"; + abstract void setOpen(boolean open); abstract boolean isPresent(); abstract boolean isOpen(); @@ -50,6 +56,8 @@ abstract class DrawerController implements DrawerListener { View drawer = activity.findViewById(R.id.drawer_roots); Toolbar toolbar = (Toolbar) activity.findViewById(R.id.roots_toolbar); + drawer.getLayoutParams().width = calculateDrawerWidth(activity); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( activity, layout, @@ -67,6 +75,19 @@ abstract class DrawerController implements DrawerListener { return new DummyDrawerController(); } + private static int calculateDrawerWidth(Activity activity) { + // Material design specification for navigation drawer: + // https://www.google.com/design/spec/patterns/navigation-drawer.html + float width = Display.screenWidth(activity) - Display.actionBarHeight(activity); + float maxWidth = activity.getResources().getDimension(R.dimen.max_drawer_width); + int finalWidth = (int) ((width > maxWidth ? maxWidth : width)); + + if (DEBUG) + Log.d(TAG, "Calculated drawer width:" + (finalWidth / Display.density(activity))); + + return finalWidth; + } + /** * Runtime controller that manages a real drawer. */ diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 19679231c130..ca1b444b3531 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -63,6 +63,7 @@ import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -526,7 +527,8 @@ public class DirectoryFragment extends Fragment getActivity().getWindow().setStatusBarColor(color.data); if (mActionMode != null) { - mActionMode.setTitle(String.valueOf(mSelected.size())); + mActionMode.setTitle(Shared.getQuantityString(getActivity(), + R.plurals.elements_selected, mSelected.size())); } } @@ -544,6 +546,8 @@ public class DirectoryFragment extends Fragment @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + int size = mSelectionManager.getSelection().size(); mode.getMenuInflater().inflate(R.menu.mode_directory, menu); mode.setTitle(TextUtils.formatSelectedCount(size)); @@ -586,18 +590,17 @@ public class DirectoryFragment extends Fragment case R.id.menu_share: shareDocuments(selection); - mode.finish(); return true; case R.id.menu_delete: - // Exit selection mode first, so we avoid deselecting deleted documents. - mode.finish(); - deleteDocuments(selection); + // Pass mode along to the delete function so it can + // end action mode when documents are deleted. + // It won't end action mode if user cancels the delete. + deleteDocuments(selection, mode); return true; case R.id.menu_copy_to: transferDocuments(selection, FileOperationService.OPERATION_COPY); - mode.finish(); return true; case R.id.menu_move_to: @@ -615,8 +618,10 @@ public class DirectoryFragment extends Fragment return true; case R.id.menu_rename: - renameDocuments(selection); + // Exit selection mode first, so we avoid deselecting deleted + // (renamed) documents. mode.finish(); + renameDocuments(selection); return true; default: @@ -700,7 +705,7 @@ public class DirectoryFragment extends Fragment }.execute(selected); } - private void deleteDocuments(final Selection selected) { + private void deleteDocuments(final Selection selected, final ActionMode mode) { assert(!selected.isEmpty()); final DocumentInfo srcParent = getDisplayState().stack.peek(); @@ -732,7 +737,15 @@ public class DirectoryFragment extends Fragment android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { - // Hide the files in the UI. + // Finish selection mode first which clears selection so we + // don't end up trying to deselect deleted documents. + // This is done here, rather in the onActionItemClicked + // so we can avoid de-selecting items in the case where + // the user cancels the delete. + mode.finish(); + // Hide the files in the UI...since the operation + // might be queued up on FileOperationService. + // We're walking a line here. mAdapter.hide(selected.getAll()); FileOperations.delete( getActivity(), docs, srcParent, getDisplayState().stack); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java index 5f33b32d7d54..9a06807f02cc 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/RenameDocumentUiTest.java @@ -16,9 +16,8 @@ package com.android.documentsui; -import static com.android.documentsui.StubProvider.ROOT_0_ID; - import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.Suppress; @LargeTest public class RenameDocumentUiTest extends ActivityTest<FilesActivity> { @@ -73,6 +72,7 @@ public class RenameDocumentUiTest extends ActivityTest<FilesActivity> { device.pressBack(); } + @Suppress public void testRenameFile_OkButton() throws Exception { bots.directory.selectDocument(fileName1); bots.main.openOverflowMenu(); @@ -101,6 +101,7 @@ public class RenameDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertDocumentsCount(4); } + @Suppress public void testRenameFile_Cancel() throws Exception { bots.directory.selectDocument(fileName1); bots.main.openOverflowMenu(); diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index ae2c6e71bfc3..ef397f664407 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -677,7 +677,7 @@ <!-- Services settings screen, setting option summary for the user to go to the screen to view running services --> <string name="runningservices_settings_summary">View and control currently running services</string> - <!-- Developer settings: enable WebView multiprocess name [CHAR LIMIT=30] --> + <!-- Developer settings: enable WebView multiprocess name [CHAR LIMIT=50] --> <string name="enable_webview_multiprocess">Enable multiprocess WebView</string> <!-- Developer settings: enable WebView multiprocess summary [CHAR LIMIT=60] --> <string name="enable_webview_multiprocess_desc">Run WebView renderers in an isolated process.</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java deleted file mode 100644 index 4c0450e38fa9..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.support.v7.preference.DropDownPreference; -import android.support.v7.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.Spinner; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - -public class RestrictedDropDownPreference extends DropDownPreference { - private Spinner mSpinner; - private final Drawable mRestrictedPadlock; - private final int mRestrictedPadlockPadding; - private List<RestrictedItem> mRestrictedItems = new ArrayList<>(); - - public RestrictedDropDownPreference(Context context) { - this(context, null); - } - - public RestrictedDropDownPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - mRestrictedPadlock = RestrictedLockUtils.getRestrictedPadlock(context); - mRestrictedPadlockPadding = context.getResources().getDimensionPixelSize( - R.dimen.restricted_icon_padding); - } - - private final OnItemSelectedListener mItemSelectedListener = new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View v, int position, long id) { - if (position >= 0) { - String value = getEntryValues()[position].toString(); - RestrictedItem item = getRestrictedItemForEntryValue(value); - if (item != null) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), - item.enforcedAdmin); - mSpinner.setSelection(findIndexOfValue(getValue())); - } else if (!value.equals(getValue()) && callChangeListener(value)) { - setValue(value); - } - } - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - // noop - } - }; - - @Override - protected ArrayAdapter createAdapter() { - return new RestrictedArrayItemAdapter(getContext()); - } - - @Override - public void setValue(String value) { - if (getRestrictedItemForEntryValue(value) != null) { - return; - } - super.setValue(value); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - mSpinner = (Spinner) view.itemView.findViewById(R.id.spinner); - mSpinner.setOnItemSelectedListener(mItemSelectedListener); - } - - private class RestrictedArrayItemAdapter extends ArrayAdapter<String> { - public RestrictedArrayItemAdapter(Context context) { - super(context, R.layout.spinner_dropdown_restricted_item); - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - TextView view = (TextView) super.getView(position, convertView, parent); - CharSequence entry = getItem(position); - boolean isEntryRestricted = isRestrictedForEntry(entry); - RestrictedLockUtils.setTextViewPadlock(getContext(), view, isEntryRestricted); - view.setEnabled(!isEntryRestricted); - return view; - } - } - - private boolean isRestrictedForEntry(CharSequence entry) { - if (entry == null) { - return false; - } - for (RestrictedItem item : mRestrictedItems) { - if (entry.equals(item.entry)) { - return true; - } - } - return false; - } - - private RestrictedItem getRestrictedItemForEntryValue(CharSequence entryValue) { - if (entryValue == null) { - return null; - } - for (RestrictedItem item : mRestrictedItems) { - if (entryValue.equals(item.entryValue)) { - return item; - } - } - return null; - } - - public void addRestrictedItem(RestrictedItem item) { - mRestrictedItems.add(item); - } - - public static class RestrictedItem { - public CharSequence entry; - public CharSequence entryValue; - public EnforcedAdmin enforcedAdmin; - - public RestrictedItem(CharSequence entry, CharSequence entryValue, - EnforcedAdmin enforcedAdmin) { - this.entry = entry; - this.entryValue = entryValue; - this.enforcedAdmin = enforcedAdmin; - } - } -}
\ No newline at end of file diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 51d8ca03d4af..978ca9466ff1 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -155,6 +155,9 @@ <!-- Default for Settings.Secure.LONG_PRESS_TIMEOUT_MILLIS --> <integer name="def_long_press_timeout_millis">500</integer> + <!-- Default for Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD --> + <bool name="def_show_ime_with_hard_keyboard">false</bool> + <!-- Default for Settings.System.POINTER_SPEED --> <integer name="def_pointer_speed">0</integer> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index a424d554729b..987b5ea2db09 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1671,16 +1671,16 @@ public class SettingsProvider extends ContentProvider { private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper, SQLiteDatabase database, int userId) { - // Move over the global settings if owner. - if (userId == UserHandle.USER_SYSTEM) { - final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId); - ensureSettingsStateLocked(globalKey); - SettingsState globalSettings = mSettingsStates.get(globalKey); - migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL); - globalSettings.persistSyncLocked(); - } + // Move over the system settings. + final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId); + ensureSettingsStateLocked(systemKey); + SettingsState systemSettings = mSettingsStates.get(systemKey); + migrateLegacySettingsLocked(systemSettings, database, TABLE_SYSTEM); + systemSettings.persistSyncLocked(); // Move over the secure settings. + // Do this after System settings, since this is the first thing we check when deciding + // to skip over migration from db to xml for a secondary user. final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId); ensureSettingsStateLocked(secureKey); SettingsState secureSettings = mSettingsStates.get(secureKey); @@ -1688,12 +1688,16 @@ public class SettingsProvider extends ContentProvider { ensureSecureSettingAndroidIdSetLocked(secureSettings); secureSettings.persistSyncLocked(); - // Move over the system settings. - final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId); - ensureSettingsStateLocked(systemKey); - SettingsState systemSettings = mSettingsStates.get(systemKey); - migrateLegacySettingsLocked(systemSettings, database, TABLE_SYSTEM); - systemSettings.persistSyncLocked(); + // Move over the global settings if owner. + // Do this last, since this is the first thing we check when deciding + // to skip over migration from db to xml for owner user. + if (userId == UserHandle.USER_SYSTEM) { + final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId); + ensureSettingsStateLocked(globalKey); + SettingsState globalSettings = mSettingsStates.get(globalKey); + migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL); + globalSettings.persistSyncLocked(); + } // Drop the database as now all is moved and persisted. if (DROP_DATABASE_ON_MIGRATION) { @@ -1936,7 +1940,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 124; + private static final int SETTINGS_VERSION = 125; private final int mUserId; @@ -2116,6 +2120,22 @@ public class SettingsProvider extends ContentProvider { currentVersion = 124; } + if (currentVersion == 124) { + // Version 124: allow OEMs to set a default value for whether IME should be + // shown when a physical keyboard is connected. + final SettingsState secureSettings = getSecureSettingsLocked(userId); + Setting currentSetting = secureSettings.getSettingLocked( + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD); + if (currentSetting == null) { + secureSettings.insertSettingLocked( + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, + getContext().getResources().getBoolean( + R.bool.def_show_ime_with_hard_keyboard) ? "1" : "0", + SettingsState.SYSTEM_PACKAGE_NAME); + } + currentVersion = 125; + } + // vXXX: Add new settings above this point. // Return the current version. diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 0fad11357bd9..1c6a0710d18f 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -56,7 +56,6 @@ import android.app.PendingIntent; import android.app.Service; import android.content.ClipData; import android.content.Context; -import android.content.ContextWrapper; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; @@ -206,7 +205,7 @@ public class BugreportProgressService extends Service { mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread"); mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread"); - mScreenshotsDir = new File(new ContextWrapper(mContext).getFilesDir(), SCREENSHOT_DIR); + mScreenshotsDir = new File(getFilesDir(), SCREENSHOT_DIR); if (!mScreenshotsDir.exists()) { Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots"); if (!mScreenshotsDir.mkdir()) { @@ -505,9 +504,9 @@ public class BugreportProgressService extends Service { Log.d(TAG, "Removing ID " + id); mProcesses.remove(id); } - stopSelfWhenDone(); Log.v(TAG, "stopProgress(" + id + "): cancel notification"); NotificationManager.from(mContext).cancel(TAG, id); + stopSelfWhenDone(); } /** @@ -877,6 +876,8 @@ public class BugreportProgressService extends Service { info = sharedInfo; Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes (" + mProcesses + "), using info from intent instead (" + info + ")"); + } else { + Log.v(TAG, "shareBugReport(): id " + id + " info = " + info); } addDetailsToZipFile(mContext, info); @@ -1532,6 +1533,7 @@ public class BugreportProgressService extends Service { final File newFile; if (!newName.equals(oldName)) { final File renamedFile = new File(screenshotDir, newName); + Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile); newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile; } else { Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen. diff --git a/packages/SystemUI/res/layout/global_screenshot.xml b/packages/SystemUI/res/layout/global_screenshot.xml index 8b337eada622..c1fe1a806d11 100644 --- a/packages/SystemUI/res/layout/global_screenshot.xml +++ b/packages/SystemUI/res/layout/global_screenshot.xml @@ -33,4 +33,10 @@ android:layout_height="match_parent" android:src="@android:color/white" android:visibility="gone" /> + <com.android.systemui.screenshot.ScreenshotSelectorView + android:id="@+id/global_screenshot_selector" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + android:pointerShape="crosshair"/> </FrameLayout> diff --git a/packages/SystemUI/src/com/android/systemui/classifier/AccelerationClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/AccelerationClassifier.java index 86bea873b737..bad739fdc764 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/AccelerationClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/AccelerationClassifier.java @@ -36,6 +36,11 @@ public class AccelerationClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "ACC"; + } + + @Override public void onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/AnglesClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/AnglesClassifier.java index dba731a9b88c..526e5fa78d35 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/AnglesClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/AnglesClassifier.java @@ -54,6 +54,11 @@ public class AnglesClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "ANG"; + } + + @Override public void onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java index 89d20defc7db..cb761a9b1314 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java @@ -48,4 +48,6 @@ public abstract class Classifier { */ public void onSensorChanged(SensorEvent event) { } + + public abstract String getTag(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DirectionClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DirectionClassifier.java index 299d0e3634b5..610e21983b5d 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/DirectionClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/DirectionClassifier.java @@ -25,6 +25,11 @@ public class DirectionClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "DIR"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { Point firstPoint = stroke.getPoints().get(0); Point lastPoint = stroke.getPoints().get(stroke.getPoints().size() - 1); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DurationCountClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DurationCountClassifier.java index 892469428f4a..77fda2001f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/DurationCountClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/DurationCountClassifier.java @@ -25,6 +25,11 @@ public class DurationCountClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "DUR"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { return DurationCountEvaluator.evaluate(stroke.getDurationSeconds() / stroke.getCount()); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/EndPointLengthClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/EndPointLengthClassifier.java index 78bc0ddf1942..de8a18860c38 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/EndPointLengthClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/EndPointLengthClassifier.java @@ -24,6 +24,11 @@ public class EndPointLengthClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "END_LNGTH"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { return EndPointLengthEvaluator.evaluate(stroke.getEndPointLength()); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/EndPointRatioClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/EndPointRatioClassifier.java index 652d969f7256..9b6ddc8ffb2f 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/EndPointRatioClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/EndPointRatioClassifier.java @@ -26,6 +26,11 @@ public class EndPointRatioClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "END_RTIO"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { float ratio; if (stroke.getTotalLength() == 0.0f) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java new file mode 100644 index 000000000000..1338d9d54793 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.classifier; + +import android.app.ActivityThread; +import android.app.Application; +import android.os.Build; +import android.os.SystemProperties; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.Date; +import java.util.Locale; + +/** + * Keeps track of interesting falsing data. + * + * By default the log only gets collected on userdebug builds. To turn it on on user: + * adb shell setprop debug.falsing_log true + * + * The log gets dumped as part of the SystemUI services. To dump on demand: + * adb shell dumpsys activity service com.android.systemui SystemBars | grep -A 999 FALSING | less + * + * To dump into logcat: + * adb shell setprop debug.falsing_logcat true + * + * To adjust the log buffer size: + * adb shell setprop debug.falsing_log_size 200 + */ +public class FalsingLog { + public static final boolean ENABLED = SystemProperties.getBoolean("debug.falsing_log", + Build.IS_DEBUGGABLE); + private static final boolean LOGCAT = SystemProperties.getBoolean("debug.falsing_logcat", + false); + + public static final boolean VERBOSE = false; + + private static final int MAX_SIZE = SystemProperties.getInt("debug.falsing_log_size", 100); + + private static final String TAG = "FalsingLog"; + + private final ArrayDeque<String> mLog = new ArrayDeque<>(MAX_SIZE); + private final SimpleDateFormat mFormat = new SimpleDateFormat("MM-dd HH:mm:ss", Locale.US); + + private static FalsingLog sInstance; + + private FalsingLog() { + } + + public static void v(String tag, String s) { + if (!VERBOSE) { + return; + } + if (LOGCAT) { + Log.v(TAG, tag + "\t" + s); + } + log("V", tag, s); + } + + public static void i(String tag, String s) { + if (LOGCAT) { + Log.i(TAG, tag + "\t" + s); + } + log("I", tag, s); + } + + public static void w(String tag, String s) { + if (LOGCAT) { + Log.w(TAG, tag + "\t" + s); + } + log("W", tag, s); + } + + public static void e(String tag, String s) { + if (LOGCAT) { + Log.e(TAG, tag + "\t" + s); + } + log("E", tag, s); + } + + public static synchronized void log(String level, String tag, String s) { + if (!ENABLED) { + return; + } + if (sInstance == null) { + sInstance = new FalsingLog(); + } + + if (sInstance.mLog.size() >= MAX_SIZE) { + sInstance.mLog.removeFirst(); + } + String entry = new StringBuilder().append(sInstance.mFormat.format(new Date())) + .append(" ").append(level).append(" ") + .append(tag).append(" ").append(s).toString(); + sInstance.mLog.add(entry); + } + + public static synchronized void dump(PrintWriter pw) { + pw.println("FALSING LOG:"); + if (!ENABLED) { + pw.println("Disabled, to enable: setprop debug.falsing_log 1"); + pw.println(); + return; + } + if (sInstance == null || sInstance.mLog.isEmpty()) { + pw.println("<empty>"); + pw.println(); + return; + } + for (String s : sInstance.mLog) { + pw.println(s); + } + pw.println(); + } + + public static synchronized void wtf(String tag, String s) { + if (!ENABLED) { + return; + } + e(tag, s); + + Application application = ActivityThread.currentApplication(); + String fileMessage = ""; + if (Build.IS_DEBUGGABLE && application != null) { + File f = new File(application.getDataDir(), "falsing-" + + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".txt"); + PrintWriter pw = null; + try { + pw = new PrintWriter(f); + dump(pw); + pw.close(); + fileMessage = "Log written to " + f.getAbsolutePath(); + } catch (IOException e) { + Log.e(TAG, "Unable to write falsing log", e); + } finally { + if (pw != null) { + pw.close(); + } + } + } else { + Log.e(TAG, "Unable to write log, build must be debuggable."); + } + + Log.wtf(TAG, tag + " " + s + "; " + fileMessage); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java index c09376bdd37e..937f7d319432 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java @@ -31,6 +31,8 @@ import android.view.MotionEvent; import com.android.systemui.analytics.DataCollector; import com.android.systemui.statusbar.StatusBarState; +import java.io.PrintWriter; + /** * When the phone is locked, listens to touch, sensor and phone events and sends them to * DataCollector and HumanInteractionClassifier. @@ -102,8 +104,14 @@ public class FalsingManager implements SensorEventListener { } private boolean shouldSessionBeActive() { - return isEnabled() && mScreenOn && - (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED); + if (FalsingLog.ENABLED && FalsingLog.VERBOSE) + FalsingLog.v("shouldBeActive", new StringBuilder() + .append("enabled=").append(isEnabled() ? 1 : 0) + .append(" mScreenOn=").append(mScreenOn ? 1 : 0) + .append(" mState=").append(StatusBarState.toShortString(mState)) + .toString() + ); + return isEnabled() && mScreenOn && (mState == StatusBarState.KEYGUARD); } private boolean sessionEntrypoint() { @@ -122,6 +130,9 @@ public class FalsingManager implements SensorEventListener { } private void onSessionStart() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onSessionStart", "classifierEnabled=" + isClassiferEnabled()); + } mBouncerOn = false; mSessionActive = true; @@ -154,6 +165,16 @@ public class FalsingManager implements SensorEventListener { * @return true if the classifier determined that this is not a human interacting with the phone */ public boolean isFalseTouch() { + if (FalsingLog.ENABLED) { + if (!mSessionActive) { + FalsingLog.wtf("isFalseTouch", new StringBuilder() + .append("Session is not active, yet there's a query for a false touch.") + .append(" enabled=").append(isEnabled() ? 1 : 0) + .append(" mScreenOn=").append(mScreenOn ? 1 : 0) + .append(" mState=").append(StatusBarState.toShortString(mState)) + .toString()); + } + } return mHumanInteractionClassifier.isFalseTouch(); } @@ -173,6 +194,12 @@ public class FalsingManager implements SensorEventListener { } public void setStatusBarState(int state) { + if (FalsingLog.ENABLED) { + FalsingLog.i("setStatusBarState", new StringBuilder() + .append("from=").append(StatusBarState.toShortString(mState)) + .append(" to=").append(StatusBarState.toShortString(state)) + .toString()); + } mState = state; if (shouldSessionBeActive()) { sessionEntrypoint(); @@ -182,6 +209,11 @@ public class FalsingManager implements SensorEventListener { } public void onScreenTurningOn() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onScreenTurningOn", new StringBuilder() + .append("from=").append(mScreenOn ? 1 : 0) + .toString()); + } mScreenOn = true; if (sessionEntrypoint()) { mDataCollector.onScreenTurningOn(); @@ -189,6 +221,11 @@ public class FalsingManager implements SensorEventListener { } public void onScreenOnFromTouch() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onScreenOnFromTouch", new StringBuilder() + .append("from=").append(mScreenOn ? 1 : 0) + .toString()); + } mScreenOn = true; if (sessionEntrypoint()) { mDataCollector.onScreenOnFromTouch(); @@ -196,17 +233,30 @@ public class FalsingManager implements SensorEventListener { } public void onScreenOff() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onScreenOff", new StringBuilder() + .append("from=").append(mScreenOn ? 1 : 0) + .toString()); + } mDataCollector.onScreenOff(); mScreenOn = false; sessionExitpoint(false /* force */); } public void onSucccessfulUnlock() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onSucccessfulUnlock", ""); + } mDataCollector.onSucccessfulUnlock(); sessionExitpoint(true /* force */); } public void onBouncerShown() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onBouncerShown", new StringBuilder() + .append("from=").append(mBouncerOn ? 1 : 0) + .toString()); + } if (!mBouncerOn) { mBouncerOn = true; mDataCollector.onBouncerShown(); @@ -214,6 +264,11 @@ public class FalsingManager implements SensorEventListener { } public void onBouncerHidden() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onBouncerHidden", new StringBuilder() + .append("from=").append(mBouncerOn ? 1 : 0) + .toString()); + } if (mBouncerOn) { mBouncerOn = false; mDataCollector.onBouncerHidden(); @@ -221,6 +276,9 @@ public class FalsingManager implements SensorEventListener { } public void onQsDown() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onQsDown", ""); + } mHumanInteractionClassifier.setType(Classifier.QUICK_SETTINGS); mDataCollector.onQsDown(); } @@ -230,6 +288,9 @@ public class FalsingManager implements SensorEventListener { } public void onTrackingStarted() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onTrackingStarted", ""); + } mHumanInteractionClassifier.setType(Classifier.UNLOCK); mDataCollector.onTrackingStarted(); } @@ -251,6 +312,9 @@ public class FalsingManager implements SensorEventListener { } public void onNotificatonStartDraggingDown() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onNotificatonStartDraggingDown", ""); + } mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DRAG_DOWN); mDataCollector.onNotificatonStartDraggingDown(); } @@ -264,6 +328,9 @@ public class FalsingManager implements SensorEventListener { } public void onNotificatonStartDismissing() { + if (FalsingLog.ENABLED) { + FalsingLog.i("onNotificatonStartDismissing", ""); + } mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS); mDataCollector.onNotificatonStartDismissing(); } @@ -281,6 +348,9 @@ public class FalsingManager implements SensorEventListener { } public void onAffordanceSwipingStarted(boolean rightCorner) { + if (FalsingLog.ENABLED) { + FalsingLog.i("onAffordanceSwipingStarted", ""); + } if (rightCorner) { mHumanInteractionClassifier.setType(Classifier.RIGHT_AFFORDANCE); } else { @@ -311,4 +381,14 @@ public class FalsingManager implements SensorEventListener { mHumanInteractionClassifier.onTouchEvent(event); } } + + public void dump(PrintWriter pw) { + pw.println("FALSING MANAGER"); + pw.print("classifierEnabled="); pw.println(isClassiferEnabled() ? 1 : 0); + pw.print("mSessionActive="); pw.println(mSessionActive ? 1 : 0); + pw.print("mBouncerOn="); pw.println(mSessionActive ? 1 : 0); + pw.print("mState="); pw.println(StatusBarState.toShortString(mState)); + pw.print("mScreenOn="); pw.println(mScreenOn ? 1 : 0); + pw.println(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java index 45eb9ad3115a..5e35d76f24f0 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java @@ -23,6 +23,7 @@ import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; import android.util.DisplayMetrics; +import android.util.Log; import android.view.MotionEvent; import java.util.ArrayDeque; @@ -43,14 +44,12 @@ public class HumanInteractionClassifier extends Classifier { private final Handler mHandler = new Handler(); private final Context mContext; - private ArrayList<StrokeClassifier> mStrokeClassifiers = new ArrayList<>(); - private ArrayList<GestureClassifier> mGestureClassifiers = new ArrayList<>(); - private ArrayDeque<MotionEvent> mBufferedEvents = new ArrayDeque<>(); - private final int mStrokeClassifiersSize; - private final int mGestureClassifiersSize; + private final StrokeClassifier[] mStrokeClassifiers; + private final GestureClassifier[] mGestureClassifiers; + private final ArrayDeque<MotionEvent> mBufferedEvents = new ArrayDeque<>(); + private final HistoryEvaluator mHistoryEvaluator; private final float mDpi; - private HistoryEvaluator mHistoryEvaluator; private boolean mEnableClassifier = false; private int mCurrentType = Classifier.GENERIC; @@ -68,25 +67,27 @@ public class HumanInteractionClassifier extends Classifier { // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi // were to be used separately. Due negligible differences in xdpi and ydpi we can just // take the average. + // TODO: make this respect DPI changes. mDpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f; mClassifierData = new ClassifierData(mDpi); mHistoryEvaluator = new HistoryEvaluator(); - mStrokeClassifiers.add(new AnglesClassifier(mClassifierData)); - mStrokeClassifiers.add(new SpeedClassifier(mClassifierData)); - mStrokeClassifiers.add(new DurationCountClassifier(mClassifierData)); - mStrokeClassifiers.add(new EndPointRatioClassifier(mClassifierData)); - mStrokeClassifiers.add(new EndPointLengthClassifier(mClassifierData)); - mStrokeClassifiers.add(new AccelerationClassifier(mClassifierData)); - mStrokeClassifiers.add(new SpeedAnglesClassifier(mClassifierData)); - mStrokeClassifiers.add(new LengthCountClassifier(mClassifierData)); - mStrokeClassifiers.add(new DirectionClassifier(mClassifierData)); - - mGestureClassifiers.add(new PointerCountClassifier(mClassifierData)); - mGestureClassifiers.add(new ProximityClassifier(mClassifierData)); - - mStrokeClassifiersSize = mStrokeClassifiers.size(); - mGestureClassifiersSize = mGestureClassifiers.size(); + mStrokeClassifiers = new StrokeClassifier[]{ + new AnglesClassifier(mClassifierData), + new SpeedClassifier(mClassifierData), + new DurationCountClassifier(mClassifierData), + new EndPointRatioClassifier(mClassifierData), + new EndPointLengthClassifier(mClassifierData), + new AccelerationClassifier(mClassifierData), + new SpeedAnglesClassifier(mClassifierData), + new LengthCountClassifier(mClassifierData), + new DirectionClassifier(mClassifierData), + }; + + mGestureClassifiers = new GestureClassifier[] { + new PointerCountClassifier(mClassifierData), + new ProximityClassifier(mClassifierData) + }; mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(HIC_ENABLE), false, @@ -150,21 +151,30 @@ public class HumanInteractionClassifier extends Classifier { private void addTouchEvent(MotionEvent event) { mClassifierData.update(event); - for (int i = 0; i < mStrokeClassifiersSize; i++) { - mStrokeClassifiers.get(i).onTouchEvent(event); + for (StrokeClassifier c : mStrokeClassifiers) { + c.onTouchEvent(event); } - for (int i = 0; i < mGestureClassifiersSize; i++) { - mGestureClassifiers.get(i).onTouchEvent(event); + for (GestureClassifier c : mGestureClassifiers) { + c.onTouchEvent(event); } int size = mClassifierData.getEndingStrokes().size(); for (int i = 0; i < size; i++) { Stroke stroke = mClassifierData.getEndingStrokes().get(i); float evaluation = 0.0f; - for (int j = 0; j < mStrokeClassifiersSize; j++) { - evaluation += mStrokeClassifiers.get(j).getFalseTouchEvaluation( - mCurrentType, stroke); + StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("stroke") : null; + for (StrokeClassifier c : mStrokeClassifiers) { + float e = c.getFalseTouchEvaluation(mCurrentType, stroke); + if (FalsingLog.ENABLED) { + String tag = c.getTag(); + sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e); + } + evaluation += e; + } + + if (FalsingLog.ENABLED) { + FalsingLog.i(" addTouchEvent", sb.toString()); } mHistoryEvaluator.addStroke(evaluation); } @@ -172,8 +182,17 @@ public class HumanInteractionClassifier extends Classifier { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { float evaluation = 0.0f; - for (int i = 0; i < mGestureClassifiersSize; i++) { - evaluation += mGestureClassifiers.get(i).getFalseTouchEvaluation(mCurrentType); + StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("gesture") : null; + for (GestureClassifier c : mGestureClassifiers) { + float e = c.getFalseTouchEvaluation(mCurrentType); + if (FalsingLog.ENABLED) { + String tag = c.getTag(); + sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e); + } + evaluation += e; + } + if (FalsingLog.ENABLED) { + FalsingLog.i(" addTouchEvent", sb.toString()); } mHistoryEvaluator.addGesture(evaluation); setType(Classifier.GENERIC); @@ -184,18 +203,25 @@ public class HumanInteractionClassifier extends Classifier { @Override public void onSensorChanged(SensorEvent event) { - for (int i = 0; i < mStrokeClassifiers.size(); i++) { - mStrokeClassifiers.get(i).onSensorChanged(event); + for (Classifier c : mStrokeClassifiers) { + c.onSensorChanged(event); } - for (int i = 0; i < mGestureClassifiers.size(); i++) { - mGestureClassifiers.get(i).onSensorChanged(event); + for (Classifier c : mGestureClassifiers) { + c.onSensorChanged(event); } } public boolean isFalseTouch() { if (mEnableClassifier) { - return mHistoryEvaluator.getEvaluation() >= 5.0f; + float evaluation = mHistoryEvaluator.getEvaluation(); + boolean result = evaluation >= 5.0f; + if (FalsingLog.ENABLED) { + FalsingLog.i("isFalseTouch", new StringBuilder() + .append("eval=").append(evaluation).append(" result=") + .append(result ? 1 : 0).toString()); + } + return result; } return false; } @@ -203,4 +229,9 @@ public class HumanInteractionClassifier extends Classifier { public boolean isEnabled() { return mEnableClassifier; } + + @Override + public String getTag() { + return "HIC"; + } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/LengthCountClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/LengthCountClassifier.java index cedf4676beec..53678a6d1730 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/LengthCountClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/LengthCountClassifier.java @@ -28,6 +28,11 @@ public class LengthCountClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "LEN_CNT"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { return LengthCountEvaluator.evaluate(stroke.getTotalLength() / Math.max(1.0f, stroke.getCount() - 2)); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/PointerCountClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/PointerCountClassifier.java index 5097b635807e..136c43345c9d 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/PointerCountClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/PointerCountClassifier.java @@ -29,6 +29,11 @@ public class PointerCountClassifier extends GestureClassifier { } @Override + public String getTag() { + return "PTR_CNT"; + } + + @Override public void onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java index 69950640337f..62adfc85621b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java @@ -35,6 +35,11 @@ public class ProximityClassifier extends GestureClassifier { } @Override + public String getTag() { + return "PROX"; + } + + @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { update(event.values[0] < event.sensor.getMaximumRange(), event.timestamp); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/SpeedAnglesClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/SpeedAnglesClassifier.java index d58274d293a1..6df72b15937f 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/SpeedAnglesClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/SpeedAnglesClassifier.java @@ -41,6 +41,11 @@ public class SpeedAnglesClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "SPD_ANG"; + } + + @Override public void onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/SpeedClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/SpeedClassifier.java index 81b78c7ecdfe..01fcc377ddbd 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/SpeedClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/SpeedClassifier.java @@ -27,6 +27,11 @@ public class SpeedClassifier extends StrokeClassifier { } @Override + public String getTag() { + return "SPD"; + } + + @Override public float getFalseTouchEvaluation(int type, Stroke stroke) { float duration = (float) stroke.getDurationNanos() / NANOS_TO_SECONDS; if (duration == 0.0f) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index 6137349e179f..c8c71cb441b8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -14,11 +14,11 @@ package com.android.systemui.qs; +import android.graphics.Path; import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.View.OnLayoutChangeListener; -import android.view.animation.PathInterpolator; import android.widget.TextView; import com.android.systemui.qs.PagedTileLayout.PageListener; import com.android.systemui.qs.QSPanel.QSTileLayout; @@ -40,9 +40,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private static final String ALLOW_FANCY_ANIMATION = "sysui_qs_fancy_anim"; private static final String MOVE_FULL_ROWS = "sysui_qs_move_whole_rows"; - public static final PathInterpolator TRANSLATION_Y_INTERPOLATOR = - new PathInterpolator(.1f, .3f, 1, 1); - public static final float EXPANDED_TILE_DELAY = .7f; private final ArrayList<View> mAllViews = new ArrayList<>(); @@ -56,7 +53,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private boolean mOnFirstPage = true; private TouchAnimator mFirstPageAnimator; private TouchAnimator mFirstPageDelayedAnimator; - private TouchAnimator mTranslationYAnimator; + private TouchAnimator mTranslationAnimator; private TouchAnimator mNonfirstPageAnimator; private boolean mOnKeyguard; @@ -110,7 +107,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha clearAnimationState(); } } else if (MOVE_FULL_ROWS.equals(key)) { - mFullRows = newValue != null && Integer.parseInt(newValue) != 0; + mFullRows = newValue == null || Integer.parseInt(newValue) != 0; } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) { mNumQuickTiles = QuickQSPanel.getNumQuickTiles(mQsContainer.getContext()); clearAnimationState(); @@ -129,6 +126,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private void updateAnimators() { TouchAnimator.Builder firstPageBuilder = new Builder(); + TouchAnimator.Builder translationXBuilder = new Builder(); TouchAnimator.Builder translationYBuilder = new Builder(); TouchAnimator.Builder firstPageDelayedBuilder = new Builder(); Collection<QSTile<?>> tiles = mQsPanel.getHost().getTiles(); @@ -138,7 +136,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha int lastYDiff = 0; firstPageDelayedBuilder.setStartDelay(EXPANDED_TILE_DELAY); firstPageBuilder.setListener(this); - translationYBuilder.setInterpolator(TRANSLATION_Y_INTERPOLATOR); // Fade in the tiles/labels as we reach the final position. firstPageDelayedBuilder.addFloat(mQsPanel.getTileLayout(), "alpha", 0, 1); mAllViews.clear(); @@ -158,7 +155,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha final int yDiff = loc2[1] - loc1[1]; lastYDiff = yDiff; // Move the quick tile right from its location to the new one. - firstPageBuilder.addFloat(quickTileView, "translationX", 0, xDiff); + translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff); translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff); // Counteract the parent translation on the tile. So we have a static base to @@ -167,7 +164,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha // Move the real tile's label from the quick tile position to its final // location. - firstPageBuilder.addFloat(label, "translationX", -xDiff, 0); + translationXBuilder.addFloat(label, "translationX", -xDiff, 0); translationYBuilder.addFloat(label, "translationY", -yDiff, 0); mTopFiveQs.add(tileIcon); @@ -188,7 +185,13 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha if (mAllowFancy) { mFirstPageAnimator = firstPageBuilder.build(); mFirstPageDelayedAnimator = firstPageDelayedBuilder.build(); - mTranslationYAnimator = translationYBuilder.build(); + Path path = new Path(); + path.moveTo(0, 0); + path.cubicTo(0, 0, 0, 1, 1, 1); + mTranslationAnimator = new TouchAnimator.Builder() + .addPath(translationXBuilder.build(), translationYBuilder.build(), + "position", "position", path) + .build(); } mNonfirstPageAnimator = new TouchAnimator.Builder() .addFloat(mQuickQsPanel, "alpha", 1, 0) @@ -226,7 +229,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mQuickQsPanel.setAlpha(1); mFirstPageAnimator.setPosition(position); mFirstPageDelayedAnimator.setPosition(position); - mTranslationYAnimator.setPosition(position); + mTranslationAnimator.setPosition(position); } else { mNonfirstPageAnimator.setPosition(position); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java index 35ade580654a..db1724570476 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java @@ -14,7 +14,11 @@ package com.android.systemui.qs; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.util.FloatProperty; import android.util.MathUtils; +import android.util.Pair; import android.util.Property; import android.view.View; import android.view.animation.Interpolator; @@ -74,6 +78,19 @@ public class TouchAnimator { } } + private static final FloatProperty<TouchAnimator> POSITION = + new FloatProperty<TouchAnimator>("position") { + @Override + public void setValue(TouchAnimator touchAnimator, float value) { + touchAnimator.setPosition(value); + } + + @Override + public Float get(TouchAnimator touchAnimator) { + return touchAnimator.mLastT; + } + }; + public static class ListenerAdapter implements Listener { @Override public void onAnimationAtStart() { } @@ -124,6 +141,19 @@ public class TouchAnimator { return this; } + public Builder addPath(Object target, String xProp, String yProp, + Path path) { + return addPath(target, target, xProp, yProp, path); + } + + public Builder addPath(Object xTarget, Object yTarget, String xProp, String yProp, + Path path) { + add(new Pair<>(xTarget, yTarget), + KeyframeSet.ofPath(getProperty(xTarget, xProp, float.class), + getProperty(yTarget, yProp, float.class), path)); + return this; + } + private void add(Object target, KeyframeSet keyframeSet) { mTargets.add(target); mValues.add(keyframeSet); @@ -152,6 +182,9 @@ public class TouchAnimator { return View.SCALE_Y; } } + if (target instanceof TouchAnimator && "position".equals(property)) { + return POSITION; + } return Property.of(target.getClass(), cls, property); } @@ -208,6 +241,10 @@ public class TouchAnimator { public static KeyframeSet ofFloat(Property property, float... values) { return new FloatKeyframeSet((Property<?, Float>) property, values); } + + public static KeyframeSet ofPath(Property xProp, Property yProp, Path path) { + return new PathKeyframeSet<>(xProp, yProp, path); + } } private static class FloatKeyframeSet<T> extends KeyframeSet { @@ -245,4 +282,31 @@ public class TouchAnimator { mProperty.set((T) target, (int) (firstFloat + (secondFloat - firstFloat) * amount)); } } + + private static class PathKeyframeSet<T> extends KeyframeSet { + private final Property<T, Float> mXProp; + private final Property<T, Float> mYProp; + private final Path mPath; + private final PathMeasure mPathMeasure; + private final float mLength; + private final float[] mPos; + + public PathKeyframeSet(Property<T, Float> xProp, Property<T, Float> yProp, Path path) { + super(2); + mXProp = xProp; + mYProp = yProp; + mPath = path; + mPathMeasure = new PathMeasure(mPath, false); + mLength = mPathMeasure.getLength(); + mPos = new float[2]; + } + + @Override + protected void interpolate(int index, float amount, Object target) { + Pair<Object, Object> targets = (Pair<Object, Object>) target; + mPathMeasure.getPosTan(amount * mLength, mPos, null); + mXProp.set((T) targets.first, mPos[0]); + mYProp.set((T) targets.second, mPos[1]); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 0709992c07ac..db686a83f5e5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -58,6 +58,7 @@ public class CustomTile extends QSTile<QSTile.State> { private final IBinder mToken = new Binder(); private final IQSTileService mService; private final TileServiceManager mServiceManager; + private final int mUser; private boolean mListening; private boolean mBound; @@ -71,6 +72,7 @@ public class CustomTile extends QSTile<QSTile.State> { mServiceManager = host.getTileServices().getTileWrapper(this); mService = mServiceManager.getTileService(); mTile = new Tile(mComponent); + mUser = ActivityManager.getCurrentUser(); try { PackageManager pm = mContext.getPackageManager(); ServiceInfo info = pm.getServiceInfo(mComponent, 0); @@ -86,6 +88,10 @@ public class CustomTile extends QSTile<QSTile.State> { } } + public int getUser() { + return mUser; + } + public ComponentName getComponent() { return mComponent; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index c4436f488887..2aad16108f50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -85,6 +85,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements mHandler = handler; mIntent = intent; mUser = user; + if (DEBUG) Log.d(TAG, "Creating " + mIntent + " " + mUser); } public ComponentName getComponent() { @@ -116,13 +117,13 @@ public class TileLifecycleManager extends BroadcastReceiver implements if (!checkComponentState()) { return; } - if (DEBUG) Log.d(TAG, "Binding service " + mIntent); + if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser); mBindTryCount++; mContext.bindServiceAsUser(mIntent, this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, mUser); } else { - if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent); + if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser); // Give it another chance next time it needs to be bound, out of kindness. mBindTryCount = 0; mWrapper = null; @@ -350,7 +351,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements @Override public void onClick(IBinder iBinder) { - if (DEBUG) Log.d(TAG, "onClick " + iBinder); + if (DEBUG) Log.d(TAG, "onClick " + iBinder + " " + mUser); if (mWrapper == null || !mWrapper.onClick(iBinder)) { mClickBinder = iBinder; queueMessage(MSG_ON_CLICK); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java index ac23f446cdc4..3ad2cc37cd17 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java @@ -645,7 +645,8 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener if (stack != null) { stackLayout.initialize(taskStackBounds, TaskStackLayoutAlgorithm.StackState.getStackStateForStack(stack)); - mDummyStackView.setTasks(stack, false /* notifyStackChanges */); + mDummyStackView.setTasks(stack, false /* notifyStackChanges */, + false /* relayoutTaskStack */); } Rect taskViewBounds = stackLayout.getUntransformedTaskViewBounds(); if (!taskViewBounds.equals(mLastTaskViewBounds)) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index 1af5a55f7a1e..8342de547720 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -184,7 +184,8 @@ public class RecentsView extends FrameLayout { // Update the stack mTaskStackView.onResume(isResumingFromVisible); - mTaskStackView.setTasks(stack, isResumingFromVisible /* notifyStackChanges */); + mTaskStackView.setTasks(stack, isResumingFromVisible /* notifyStackChanges */, + true /* relayoutTaskStack */); if (isResumingFromVisible) { // If we are already visible, then restore the background scrim diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 9da8fee137a6..2601cd327982 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -298,7 +298,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** * Sets the stack tasks of this TaskStackView from the given TaskStack. */ - public void setTasks(TaskStack stack, boolean notifyStackChanges) { + public void setTasks(TaskStack stack, boolean notifyStackChanges, boolean relayoutTaskStack) { boolean isInitialized = mLayoutAlgorithm.isInitialized(); mStack.setTasks(getContext(), stack.computeAllTasksList(), notifyStackChanges && isInitialized); @@ -307,15 +307,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // measure/layout pass updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET); updateToInitialState(); - relayoutTaskViews(AnimationProps.IMMEDIATE); - // Rebind all the task views. This will not trigger new resources to be loaded unless - // they have actually changed - List<TaskView> taskViews = getTaskViews(); - int taskViewCount = taskViews.size(); - for (int i = 0; i < taskViewCount; i++) { - TaskView tv = taskViews.get(i); - bindTaskView(tv, tv.getTask()); + if (relayoutTaskStack) { + relayoutTaskViews(AnimationProps.IMMEDIATE); + + // Rebind all the task views. This will not trigger new resources to be loaded + // unless they have actually changed + List<TaskView> taskViews = getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); + bindTaskView(tv, tv.getTask()); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index a06700d88c15..e64354ca1408 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -39,6 +39,7 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PointF; +import android.graphics.Rect; import android.media.MediaActionSound; import android.net.Uri; import android.os.AsyncTask; @@ -407,6 +408,7 @@ class GlobalScreenshot { private Bitmap mScreenBitmap; private View mScreenshotLayout; + private ScreenshotSelectorView mScreenshotSelectorView; private ImageView mBackgroundView; private ImageView mScreenshotView; private ImageView mScreenshotFlash; @@ -437,7 +439,11 @@ class GlobalScreenshot { mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background); mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot); mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash); + mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById( + R.id.global_screenshot_selector); mScreenshotLayout.setFocusable(true); + mScreenshotSelectorView.setFocusable(true); + mScreenshotSelectorView.setFocusableInTouchMode(true); mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -449,7 +455,7 @@ class GlobalScreenshot { // Setup the window that we are going to use mWindowLayoutParams = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, - WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.TYPE_SCREENSHOT, WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN @@ -525,7 +531,8 @@ class GlobalScreenshot { /** * Takes a screenshot of the current display and shows an animation. */ - void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { + void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, + int x, int y, int width, int height) { // We need to orient the screenshot correctly (and the Surface api seems to take screenshots // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); @@ -565,6 +572,13 @@ class GlobalScreenshot { mScreenBitmap = ss; } + if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) { + // Crop the screenshot to selected region + Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height); + mScreenBitmap.recycle(); + mScreenBitmap = cropped; + } + // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); @@ -574,6 +588,71 @@ class GlobalScreenshot { statusBarVisible, navBarVisible); } + void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { + mDisplay.getRealMetrics(mDisplayMetrics); + takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels, + mDisplayMetrics.heightPixels); + } + + /** + * Displays a screenshot selector + */ + void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible, + final boolean navBarVisible) { + mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); + mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + ScreenshotSelectorView view = (ScreenshotSelectorView) v; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + view.startSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_MOVE: + view.updateSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_UP: + view.setVisibility(View.GONE); + mWindowManager.removeView(mScreenshotLayout); + final Rect rect = view.getSelectionRect(); + if (rect != null) { + if (rect.width() != 0 && rect.height() != 0) { + // Need mScreenshotLayout to handle it after the view disappears + mScreenshotLayout.post(new Runnable() { + public void run() { + takeScreenshot(finisher, statusBarVisible, navBarVisible, + rect.left, rect.top, rect.width(), rect.height()); + } + }); + } + } + + view.stopSelection(); + return true; + } + + return false; + } + }); + mScreenshotLayout.post(new Runnable() { + @Override + public void run() { + mScreenshotSelectorView.setVisibility(View.VISIBLE); + mScreenshotSelectorView.requestFocus(); + } + }); + } + + /** + * Cancels screenshot request + */ + void stopScreenshot() { + // If the selector layer still presents on screen, we remove it and resets its state. + if (mScreenshotSelectorView.getSelectionRect() != null) { + mWindowManager.removeView(mScreenshotLayout); + mScreenshotSelectorView.stopSelection(); + } + } /** * Starts the animation after taking the screenshot diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java new file mode 100644 index 000000000000..07a92460f3ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.screenshot; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +/** + * Draws a selection rectangle while taking screenshot + */ +public class ScreenshotSelectorView extends View { + private Point mStartPoint; + private Rect mSelectionRect; + private final Paint mPaintSelection, mPaintBackground; + + public ScreenshotSelectorView(Context context) { + this(context, null); + } + + public ScreenshotSelectorView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mPaintBackground = new Paint(Color.BLACK); + mPaintBackground.setAlpha(160); + mPaintSelection = new Paint(Color.TRANSPARENT); + mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + public void startSelection(int x, int y) { + mStartPoint = new Point(x, y); + mSelectionRect = new Rect(x, y, x, y); + } + + public void updateSelection(int x, int y) { + if (mSelectionRect != null) { + mSelectionRect.left = Math.min(mStartPoint.x, x); + mSelectionRect.right = Math.max(mStartPoint.x, x); + mSelectionRect.top = Math.min(mStartPoint.y, y); + mSelectionRect.bottom = Math.max(mStartPoint.y, y); + invalidate(); + } + } + + public Rect getSelectionRect() { + return mSelectionRect; + } + + public void stopSelection() { + mStartPoint = null; + mSelectionRect = null; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); + if (mSelectionRect != null) { + canvas.drawRect(mSelectionRect, mPaintSelection); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 456b5fa7b0a6..4badc420d74f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -23,6 +23,7 @@ import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; +import android.view.WindowManager; public class TakeScreenshotService extends Service { private static final String TAG = "TakeScreenshotService"; @@ -32,21 +33,28 @@ public class TakeScreenshotService extends Service { private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { - switch (msg.what) { - case 1: - final Messenger callback = msg.replyTo; - if (mScreenshot == null) { - mScreenshot = new GlobalScreenshot(TakeScreenshotService.this); + final Messenger callback = msg.replyTo; + Runnable finisher = new Runnable() { + @Override + public void run() { + Message reply = Message.obtain(null, 1); + try { + callback.send(reply); + } catch (RemoteException e) { } - mScreenshot.takeScreenshot(new Runnable() { - @Override public void run() { - Message reply = Message.obtain(null, 1); - try { - callback.send(reply); - } catch (RemoteException e) { - } - } - }, msg.arg1 > 0, msg.arg2 > 0); + } + }; + if (mScreenshot == null) { + mScreenshot = new GlobalScreenshot(TakeScreenshotService.this); + } + + switch (msg.what) { + case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: + mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); + break; + case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: + mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0); + break; } } }; @@ -55,4 +63,10 @@ public class TakeScreenshotService extends Service { public IBinder onBind(Intent intent) { return new Messenger(mHandler).getBinder(); } + + @Override + public boolean onUnbind(Intent intent) { + if (mScreenshot != null) mScreenshot.stopScreenshot(); + return true; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java index 7e7fc3aa25f7..c0148c0ecb21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java @@ -41,4 +41,20 @@ public class StatusBarState { * Status bar is locked and shows the full screen user switcher. */ public static final int FULLSCREEN_USER_SWITCHER = 3; + + + public static String toShortString(int x) { + switch (x) { + case SHADE: + return "SHD"; + case SHADE_LOCKED: + return "SHD_LCK"; + case KEYGUARD: + return "KGRD"; + case FULLSCREEN_USER_SWITCHER: + return "FS_USRSW"; + default: + return "bad_value_" + x; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 9c6f2c53f9e3..d50e67aaefbe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -92,6 +92,7 @@ public class NotificationPanelView extends PanelView implements private KeyguardUserSwitcher mKeyguardUserSwitcher; private KeyguardStatusBarView mKeyguardStatusBar; protected QSContainer mQsContainer; + private DensityContainer mQsDensityContainer; private KeyguardStatusView mKeyguardStatusView; private TextView mClockView; private View mReserveNotificationSpace; @@ -217,8 +218,8 @@ public class NotificationPanelView extends PanelView implements super.onFinishInflate(); mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header); mKeyguardStatusView = (KeyguardStatusView) findViewById(R.id.keyguard_status_view); - DensityContainer container = (DensityContainer) findViewById(R.id.qs_density_container); - container.addInflateListener(new InflateListener() { + mQsDensityContainer = (DensityContainer) findViewById(R.id.qs_density_container); + mQsDensityContainer.addInflateListener(new InflateListener() { @Override public void onInflated(View v) { mQsContainer = (QSContainer) v.findViewById(R.id.quick_settings_container); @@ -2209,7 +2210,7 @@ public class NotificationPanelView extends PanelView implements protected void setVerticalPanelTranslation(float translation) { mNotificationStackScroller.setTranslationX(translation); - mQsContainer.setTranslationX(translation); + mQsDensityContainer.setTranslationX(translation); } protected void updateStackHeight(float stackHeight) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 7856f6cfbdf6..6ad7aadc0972 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -112,6 +112,7 @@ import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.SystemUIFactory; import com.android.systemui.assist.AssistManager; +import com.android.systemui.classifier.FalsingLog; import com.android.systemui.classifier.FalsingManager; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; @@ -2263,6 +2264,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mStatusBarWindowManager.setPanelExpanded(isExpanded); } + public void onScreenTurnedOff() { + mFalsingManager.onScreenOff(); + } + /** * All changes to the status bar and notifications funnel through here and are batched. */ @@ -2966,18 +2971,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args); } + FalsingManager.getInstance(mContext).dump(pw); + FalsingLog.dump(pw); + pw.println("SharedPreferences:"); for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) { pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); } } - private String hunStateToString(Entry entry) { - if (entry == null) return "null"; - if (entry.notification == null) return "corrupt"; - return entry.notification.getPackageName(); - } - private static void dumpBarTransitions(PrintWriter pw, String var, BarTransitions transitions) { pw.print(" "); pw.print(var); pw.print(".BarTransitions.mMode="); pw.println(BarTransitions.modeToString(transitions.getMode())); @@ -4186,7 +4188,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mDeviceInteractive = false; mWakeUpComingFromTouch = false; mWakeUpTouchLocation = null; - mFalsingManager.onScreenOff(); mStackScroller.setAnimationsEnabled(false); updateVisibleToUser(); if (mLaunchCameraOnFinishedGoingToSleep) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java index f894a22d4acb..5dcd393d2c06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java @@ -114,6 +114,7 @@ public final class QSTileHost implements QSTile.Host, Tunable { private final ManagedProfileController mProfileController; private final NextAlarmController mNextAlarmController; private View mHeader; + private int mCurrentUser; public QSTileHost(Context context, PhoneStatusBar statusBar, BluetoothController bluetooth, LocationController location, @@ -320,7 +321,8 @@ public final class QSTileHost implements QSTile.Host, Tunable { } if (DEBUG) Log.d(TAG, "Recreating tiles"); final List<String> tileSpecs = loadTileSpecs(mContext, newValue); - if (tileSpecs.equals(mTileSpecs)) return; + int currentUser = ActivityManager.getCurrentUser(); + if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; for (Map.Entry<String, QSTile<?>> tile : mTiles.entrySet()) { if (!tileSpecs.contains(tile.getKey())) { if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); @@ -329,15 +331,16 @@ public final class QSTileHost implements QSTile.Host, Tunable { } final LinkedHashMap<String, QSTile<?>> newTiles = new LinkedHashMap<>(); for (String tileSpec : tileSpecs) { - if (mTiles.containsKey(tileSpec)) { - QSTile<?> tile = mTiles.get(tileSpec); + QSTile<?> tile = mTiles.get(tileSpec); + if (tile != null && (!(tile instanceof CustomTile) + || ((CustomTile) tile).getUser() == currentUser)) { if (DEBUG) Log.d(TAG, "Adding " + tile); tile.removeCallbacks(); newTiles.put(tileSpec, tile); } else { if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); try { - QSTile<?> tile = createTile(tileSpec); + tile = createTile(tileSpec); if (tile != null && tile.isAvailable()) { tile.setTileSpec(tileSpec); newTiles.put(tileSpec, tile); @@ -347,6 +350,7 @@ public final class QSTileHost implements QSTile.Host, Tunable { } } } + mCurrentUser = currentUser; mTileSpecs.clear(); mTileSpecs.addAll(tileSpecs); mTiles.clear(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 1a557e428fe3..3eda3204d7a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -45,7 +45,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, public static final long ANIMATION_DURATION = 220; public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR = new PathInterpolator(0f, 0, 0.7f, 1f); - private static final float SCRIM_BEHIND_ALPHA = 0.62f; private static final float SCRIM_BEHIND_ALPHA_KEYGUARD = 0.45f; private static final float SCRIM_BEHIND_ALPHA_UNLOCKING = 0.2f; @@ -60,6 +59,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, private final UnlockMethodCache mUnlockMethodCache; private final View mHeadsUpScrim; + private float mScrimBehindAlpha = SCRIM_BEHIND_ALPHA; + private float mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD; + private float mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING; + protected boolean mKeyguardShowing; private float mFraction; @@ -86,6 +89,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, private boolean mForceHideScrims; private boolean mSkipFirstFrame; private boolean mDontAnimateBouncerChanges; + private boolean mKeyguardFadingOutInProgress; public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, View headsUpScrim) { mScrimBehind = scrimBehind; @@ -101,6 +105,19 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, scheduleUpdate(); } + public void setShowScrimBehind(boolean show) { + if (show) { + mScrimBehindAlpha = SCRIM_BEHIND_ALPHA; + mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD; + mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING; + } else { + mScrimBehindAlpha = 0; + mScrimBehindAlphaKeyguard = 0; + mScrimBehindAlphaUnlocking = 0; + } + scheduleUpdate(); + } + public void onTrackingStarted() { mExpanding = true; mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer(); @@ -229,7 +246,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, fraction = (float) Math.pow(fraction, 0.8f); behindFraction = (float) Math.pow(behindFraction, 0.8f); setScrimInFrontColor(fraction * SCRIM_IN_FRONT_ALPHA); - setScrimBehindColor(behindFraction * SCRIM_BEHIND_ALPHA_KEYGUARD); + setScrimBehindColor(behindFraction * mScrimBehindAlphaKeyguard); } else if (mBouncerShowing) { setScrimInFrontColor(SCRIM_IN_FRONT_ALPHA); setScrimBehindColor(0f); @@ -237,8 +254,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, float fraction = Math.max(0, Math.min(mFraction, 1)); setScrimInFrontColor(0f); setScrimBehindColor(fraction - * (SCRIM_BEHIND_ALPHA_KEYGUARD - SCRIM_BEHIND_ALPHA_UNLOCKING) - + SCRIM_BEHIND_ALPHA_UNLOCKING); + * (mScrimBehindAlphaKeyguard - mScrimBehindAlphaUnlocking) + + mScrimBehindAlphaUnlocking); } } @@ -251,7 +268,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } else { // woo, special effects final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f)))); - setScrimBehindColor(k * SCRIM_BEHIND_ALPHA); + setScrimBehindColor(k * mScrimBehindAlpha); } } @@ -326,12 +343,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, if (mOnAnimationFinished != null) { mOnAnimationFinished.run(); mOnAnimationFinished = null; + mKeyguardFadingOutInProgress = false; } scrim.setTag(TAG_KEY_ANIM, null); scrim.setTag(TAG_KEY_ANIM_TARGET, null); } }); anim.start(); + if (mAnimateKeyguardFadingOut) { + mKeyguardFadingOutInProgress = true; + } if (mSkipFirstFrame) { anim.setCurrentPlayTime(16); } @@ -366,6 +387,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, && mOnAnimationFinished != null) { mOnAnimationFinished.run(); mOnAnimationFinished = null; + mKeyguardFadingOutInProgress = false; } } @@ -406,6 +428,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } private void updateScrim(boolean animate, View scrim, float alpha, float currentAlpha) { + if (mKeyguardFadingOutInProgress) { + return; + } + ValueAnimator previousAnimator = StackStateAnimator.getChildTag(scrim, TAG_KEY_ANIM); float animEndValue = -1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 0e84f733cad4..2ba1562c39e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -212,6 +212,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void onScreenTurnedOff() { mScreenTurnedOn = false; + mPhoneStatusBar.onScreenTurnedOff(); } public void notifyDeviceWakeUpRequested() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index cca374602b63..824b8b70eede 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -3617,7 +3617,6 @@ public class NotificationStackScrollLayout extends ViewGroup // ANIMATION_TYPE_DIMMED new AnimationFilter() - .animateY() .animateDimmed(), // ANIMATION_TYPE_CHANGE_POSITION diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java index 123165e3ba3c..ea15b8164214 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java @@ -34,6 +34,7 @@ import android.media.session.MediaSessionManager; import android.os.Debug; import android.os.Handler; import android.os.RemoteException; +import android.os.SystemProperties; import android.util.Log; import com.android.systemui.Prefs; @@ -51,7 +52,8 @@ import static com.android.systemui.Prefs.Key.TV_PICTURE_IN_PICTURE_ONBOARDING_SH public class PipManager { private static final String TAG = "PipManager"; private static final boolean DEBUG = false; - private static final boolean DEBUG_FORCE_ONBOARDING = false; + private static final boolean DEBUG_FORCE_ONBOARDING = + SystemProperties.getBoolean("debug.tv.pip_force_onboarding", false); private static PipManager sPipManager; diff --git a/rs/jni/android_renderscript_RenderScript.cpp b/rs/jni/android_renderscript_RenderScript.cpp index 4877a378aaf4..4e667c659133 100644 --- a/rs/jni/android_renderscript_RenderScript.cpp +++ b/rs/jni/android_renderscript_RenderScript.cpp @@ -1062,7 +1062,7 @@ nElementCreate(JNIEnv *_env, jobject _this, jlong con, jlong type, jint kind, jb type, kind, norm, size); } return (jlong)(uintptr_t)rsElementCreate((RsContext)con, (RsDataType)type, (RsDataKind)kind, - norm, size, true); + norm, size); } static jlong @@ -1101,7 +1101,7 @@ nElementCreate2(JNIEnv *_env, jobject _this, jlong con, jlong id = (jlong)(uintptr_t)rsElementCreate2((RsContext)con, (const RsElement *)ids, fieldCount, nameArray, fieldCount * sizeof(size_t), sizeArray, - (const uint32_t *)arraySizes, fieldCount, true); + (const uint32_t *)arraySizes, fieldCount); free(ids); free(arraySizes); @@ -1175,7 +1175,7 @@ nTypeCreate(JNIEnv *_env, jobject _this, jlong con, jlong eid, } return (jlong)(uintptr_t)rsTypeCreate((RsContext)con, (RsElement)eid, dimx, dimy, dimz, mips, - faces, yuv, true); + faces, yuv); } static void @@ -1211,7 +1211,7 @@ nAllocationCreateTyped(JNIEnv *_env, jobject _this, jlong con, jlong type, jint } return (jlong)(uintptr_t) rsAllocationCreateTyped((RsContext)con, (RsType)type, (RsAllocationMipmapControl)mips, - (uint32_t)usage, (uintptr_t)pointer, true); + (uint32_t)usage, (uintptr_t)pointer); } static void @@ -1316,7 +1316,7 @@ nAllocationCreateFromBitmap(JNIEnv *_env, jobject _this, jlong con, jlong type, const void* ptr = bitmap.getPixels(); jlong id = (jlong)(uintptr_t)rsAllocationCreateFromBitmap((RsContext)con, (RsType)type, (RsAllocationMipmapControl)mip, - ptr, bitmap.getSize(), usage, true); + ptr, bitmap.getSize(), usage); bitmap.unlockPixels(); return id; } @@ -1332,7 +1332,7 @@ nAllocationCreateBitmapBackedAllocation(JNIEnv *_env, jobject _this, jlong con, const void* ptr = bitmap.getPixels(); jlong id = (jlong)(uintptr_t)rsAllocationCreateTyped((RsContext)con, (RsType)type, (RsAllocationMipmapControl)mip, - (uint32_t)usage, (uintptr_t)ptr, true); + (uint32_t)usage, (uintptr_t)ptr); bitmap.unlockPixels(); return id; } @@ -1348,7 +1348,7 @@ nAllocationCubeCreateFromBitmap(JNIEnv *_env, jobject _this, jlong con, jlong ty const void* ptr = bitmap.getPixels(); jlong id = (jlong)(uintptr_t)rsAllocationCubeCreateFromBitmap((RsContext)con, (RsType)type, (RsAllocationMipmapControl)mip, - ptr, bitmap.getSize(), usage, true); + ptr, bitmap.getSize(), usage); bitmap.unlockPixels(); return id; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 9e6c21c3c074..b4b40ae046b4 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -3436,7 +3436,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT: case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG: case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR: - case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY: { + case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY: + case WindowManager.LayoutParams.TYPE_SCREENSHOT: { return AccessibilityWindowInfo.TYPE_SYSTEM; } diff --git a/services/core/java/com/android/server/TextServicesManagerService.java b/services/core/java/com/android/server/TextServicesManagerService.java index 3f453dc64646..306e9331671e 100644 --- a/services/core/java/com/android/server/TextServicesManagerService.java +++ b/services/core/java/com/android/server/TextServicesManagerService.java @@ -27,6 +27,7 @@ import com.android.internal.textservice.ITextServicesSessionListener; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManagerNative; import android.app.AppGlobals; @@ -80,6 +81,8 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { private final ArrayList<SpellCheckerInfo> mSpellCheckerList = new ArrayList<>(); private final HashMap<String, SpellCheckerBindGroup> mSpellCheckerBindGroups = new HashMap<>(); private final TextServicesSettings mSettings; + @NonNull + private final UserManager mUserManager; public static final class Lifecycle extends SystemService { private TextServicesManagerService mService; @@ -109,19 +112,37 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { mService.systemRunning(); } } + + @Override + public void onUnlockUser(@UserIdInt int userHandle) { + // Called on the system server's main looper thread. + // TODO: Dispatch this to a worker thread as needed. + mService.onUnlockUser(userHandle); + } } void systemRunning() { synchronized (mSpellCheckerMap) { if (!mSystemReady) { mSystemReady = true; + resetInternalState(mSettings.getCurrentUserId()); } } } void onSwitchUser(@UserIdInt int userId) { synchronized (mSpellCheckerMap) { - switchUserLocked(userId); + resetInternalState(userId); + } + } + + void onUnlockUser(@UserIdInt int userId) { + synchronized(mSpellCheckerMap) { + final int currentUserId = mSettings.getCurrentUserId(); + if (userId != currentUserId) { + return; + } + resetInternalState(currentUserId); } } @@ -129,6 +150,8 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { mSystemReady = false; mContext = context; + mUserManager = mContext.getSystemService(UserManager.class); + final IntentFilter broadcastFilter = new IntentFilter(); broadcastFilter.addAction(Intent.ACTION_USER_ADDED); broadcastFilter.addAction(Intent.ACTION_USER_REMOVED); @@ -142,14 +165,19 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { } mMonitor = new TextServicesMonitor(); mMonitor.register(context, null, true); - mSettings = new TextServicesSettings(context.getContentResolver(), userId); + final boolean useCopyOnWriteSettings = + !mSystemReady || !mUserManager.isUserUnlocked(userId); + mSettings = new TextServicesSettings(context.getContentResolver(), userId, + useCopyOnWriteSettings); - // "switchUserLocked" initializes the states for the foreground user - switchUserLocked(userId); + // "resetInternalState" initializes the states for the foreground user + resetInternalState(userId); } - private void switchUserLocked(@UserIdInt int userId) { - mSettings.setCurrentUserId(userId); + private void resetInternalState(@UserIdInt int userId) { + final boolean useCopyOnWriteSettings = + !mSystemReady || !mUserManager.isUserUnlocked(userId); + mSettings.switchCurrentUser(userId, useCopyOnWriteSettings); updateCurrentProfileIds(); unbindServiceLocked(); buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap, mSettings); @@ -166,8 +194,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { } void updateCurrentProfileIds() { - List<UserInfo> profiles = - UserManager.get(mContext).getProfiles(mSettings.getCurrentUserId()); + final List<UserInfo> profiles = mUserManager.getProfiles(mSettings.getCurrentUserId()); int[] currentProfileIds = new int[profiles.size()]; // profiles will not be null for (int i = 0; i < currentProfileIds.length; i++) { currentProfileIds[i] = profiles.get(i).id; @@ -232,6 +259,9 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { list.clear(); map.clear(); final PackageManager pm = context.getPackageManager(); + // Note: We do not specify PackageManager.MATCH_ENCRYPTION_* flags here because the default + // behavior of PackageManager is exactly what we want. It by default picks up appropriate + // services depending on the unlock state for the specified user. final List<ResolveInfo> services = pm.queryIntentServicesAsUser( new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA, settings.getCurrentUserId()); @@ -774,50 +804,36 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { synchronized(mSpellCheckerMap) { pw.println("Current Text Services Manager state:"); - pw.println(" Spell Checker Map:"); - for (Map.Entry<String, SpellCheckerInfo> ent : mSpellCheckerMap.entrySet()) { - pw.print(" "); pw.print(ent.getKey()); pw.println(":"); - SpellCheckerInfo info = ent.getValue(); - pw.print(" "); pw.print("id="); pw.println(info.getId()); - pw.print(" "); pw.print("comp="); - pw.println(info.getComponent().toShortString()); - int NS = info.getSubtypeCount(); - for (int i=0; i<NS; i++) { - SpellCheckerSubtype st = info.getSubtypeAt(i); - pw.print(" "); pw.print("Subtype #"); pw.print(i); pw.println(":"); - pw.print(" "); pw.print("locale="); pw.println(st.getLocale()); - pw.print(" "); pw.print("extraValue="); - pw.println(st.getExtraValue()); - } + pw.println(" Spell Checkers:"); + int spellCheckerIndex = 0; + for (final SpellCheckerInfo info : mSpellCheckerMap.values()) { + pw.println(" Spell Checker #" + spellCheckerIndex); + info.dump(pw, " "); + ++spellCheckerIndex; } pw.println(""); pw.println(" Spell Checker Bind Groups:"); - for (Map.Entry<String, SpellCheckerBindGroup> ent + for (final Map.Entry<String, SpellCheckerBindGroup> ent : mSpellCheckerBindGroups.entrySet()) { - SpellCheckerBindGroup grp = ent.getValue(); - pw.print(" "); pw.print(ent.getKey()); pw.print(" "); - pw.print(grp); pw.println(":"); - pw.print(" "); pw.print("mInternalConnection="); - pw.println(grp.mInternalConnection); - pw.print(" "); pw.print("mSpellChecker="); - pw.println(grp.mSpellChecker); - pw.print(" "); pw.print("mBound="); pw.print(grp.mBound); - pw.print(" mConnected="); pw.println(grp.mConnected); - int NL = grp.mListeners.size(); - for (int i=0; i<NL; i++) { - InternalDeathRecipient listener = grp.mListeners.get(i); - pw.print(" "); pw.print("Listener #"); pw.print(i); pw.println(":"); - pw.print(" "); pw.print("mTsListener="); - pw.println(listener.mTsListener); - pw.print(" "); pw.print("mScListener="); - pw.println(listener.mScListener); - pw.print(" "); pw.print("mGroup="); - pw.println(listener.mGroup); - pw.print(" "); pw.print("mScLocale="); - pw.print(listener.mScLocale); - pw.print(" mUid="); pw.println(listener.mUid); + final SpellCheckerBindGroup grp = ent.getValue(); + pw.println(" " + ent.getKey() + " " + grp + ":"); + pw.println(" " + "mInternalConnection=" + grp.mInternalConnection); + pw.println(" " + "mSpellChecker=" + grp.mSpellChecker); + pw.println(" " + "mBound=" + grp.mBound + " mConnected=" + grp.mConnected); + final int N = grp.mListeners.size(); + for (int i = 0; i < N; i++) { + final InternalDeathRecipient listener = grp.mListeners.get(i); + pw.println(" " + "Listener #" + i + ":"); + pw.println(" " + "mTsListener=" + listener.mTsListener); + pw.println(" " + "mScListener=" + listener.mScListener); + pw.println(" " + "mGroup=" + listener.mGroup); + pw.println(" " + "mScLocale=" + listener.mScLocale + + " mUid=" + listener.mUid); } } + pw.println(""); + pw.println(" mSettings:"); + mSettings.dumpLocked(pw, " "); } } @@ -1037,33 +1053,70 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { private int[] mCurrentProfileIds = new int[0]; private Object mLock = new Object(); - public TextServicesSettings(ContentResolver resolver, @UserIdInt int userId) { - mResolver = resolver; - mCurrentUserId = userId; - } + /** + * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}. + */ + private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>(); + private boolean mCopyOnWrite = false; - public void setCurrentUserId(@UserIdInt int userId) { + public TextServicesSettings(ContentResolver resolver, @UserIdInt int userId, + boolean copyOnWrite) { + mResolver = resolver; + switchCurrentUser(userId, copyOnWrite); + } + + /** + * Must be called when the current user is changed. + * + * @param userId The user ID. + * @param copyOnWrite If {@code true}, for each settings key + * (e.g. {@link Settings.Secure#SELECTED_SPELL_CHECKER}) we use the actual settings on the + * {@link Settings.Secure} until we do the first write operation. + */ + public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) { if (DBG) { Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId + ", new ime = " + getSelectedSpellChecker()); } + if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) { + mCopyOnWriteDataStore.clear(); + // TODO: mCurrentProfileIds should be cleared here. + } // TSMS settings are kept per user, so keep track of current user mCurrentUserId = userId; + mCopyOnWrite = copyOnWrite; + // TODO: mCurrentProfileIds should be updated here. } private void putString(final String key, final String str) { - Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId); + if (mCopyOnWrite) { + mCopyOnWriteDataStore.put(key, str); + } else { + Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId); + } } private String getString(final String key) { + if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) { + final String result = mCopyOnWriteDataStore.get(key); + return result != null ? result : ""; + } return Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId); } private void putInt(final String key, final int value) { - Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId); + if (mCopyOnWrite) { + mCopyOnWriteDataStore.put(key, String.valueOf(value)); + } else { + Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId); + } } private int getInt(final String key, final int defaultValue) { + if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) { + final String result = mCopyOnWriteDataStore.get(key); + return result != null ? Integer.valueOf(result) : 0; + } return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId); } @@ -1119,6 +1172,12 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { public boolean isSpellCheckerEnabled() { return getBoolean(Settings.Secure.SPELL_CHECKER_ENABLED, true); } + + public void dumpLocked(final PrintWriter pw, final String prefix) { + pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); + pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds)); + pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite); + } } // ---------------------------------------------------------------------- diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 0317641acaa4..f07c1d05e29a 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1899,7 +1899,9 @@ public final class ActivityManagerService extends ActivityManagerNative case SYSTEM_USER_UNLOCK_MSG: { final int userId = msg.arg1; mSystemServiceManager.unlockUser(userId); - mRecentTasks.loadUserRecentsLocked(userId); + synchronized (ActivityManagerService.this) { + mRecentTasks.loadUserRecentsLocked(userId); + } if (userId == UserHandle.USER_SYSTEM) { startPersistentApps(PackageManager.MATCH_ENCRYPTION_UNAWARE); } @@ -6655,7 +6657,7 @@ public final class ActivityManagerService extends ActivityManagerNative synchronized(this) { ActivityStack stack = ActivityRecord.getStackLocked(token); if (stack != null) { - ActivityRecord.activityResumedLocked(token); + stack.activityResumedLocked(token); } } Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java index 2ea5d15ec7c8..5219827d2f8e 100755 --- a/services/core/java/com/android/server/am/ActivityRecord.java +++ b/services/core/java/com/android/server/am/ActivityRecord.java @@ -1277,13 +1277,6 @@ final class ActivityRecord { } } - static void activityResumedLocked(IBinder token) { - final ActivityRecord r = ActivityRecord.forTokenLocked(token); - if (DEBUG_SAVED_STATE) Slog.i(TAG_STATES, "Resumed activity; dropping state of: " + r); - r.icicle = null; - r.haveState = false; - } - static int getTaskForActivityLocked(IBinder token, boolean onlyRoot) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); if (r == null) { diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index e50722ac04d8..39895ac54e7f 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -1088,6 +1088,13 @@ final class ActivityStack { mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS); } + final void activityResumedLocked(IBinder token) { + final ActivityRecord r = ActivityRecord.forTokenLocked(token); + if (DEBUG_SAVED_STATE) Slog.i(TAG_STATES, "Resumed activity; dropping state of: " + r); + r.icicle = null; + r.haveState = false; + } + final void activityStoppedLocked(ActivityRecord r, Bundle icicle, PersistableBundle persistentState, CharSequence description) { if (r.state != ActivityState.STOPPING) { @@ -4436,10 +4443,10 @@ final class ActivityStack { "Moving to " + (andResume ? "RESUMED" : "PAUSED") + " Relaunching " + r + " callers=" + Debug.getCallers(6)); r.forceNewConfig = false; + mStackSupervisor.activityRelaunchingLocked(r); r.app.thread.scheduleRelaunchActivity(r.appToken, results, newIntents, changes, !andResume, new Configuration(mService.mConfiguration), new Configuration(r.task.mOverrideConfig), preserveWindow); - mStackSupervisor.activityRelaunchingLocked(r); // Note: don't need to call pauseIfSleepingLocked() here, because // the caller will only pass in 'andResume' if this activity is // currently resumed, which implies we aren't sleeping. diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java index 62275a98aba1..ea85fa1de464 100644 --- a/services/core/java/com/android/server/am/TaskRecord.java +++ b/services/core/java/com/android/server/am/TaskRecord.java @@ -40,6 +40,7 @@ import android.os.Debug; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.service.voice.IVoiceInteractionSession; import android.util.DisplayMetrics; import android.util.Slog; @@ -74,6 +75,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZEABLE; import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; +import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ADD_REMOVE; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LOCKTASK; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS; @@ -444,9 +446,9 @@ final class TaskRecord { // task as having a true root activity. rootWasReset = true; } - userId = UserHandle.getUserId(info.applicationInfo.uid); - mUserSetupComplete = mService.mUserController.isUserSetupCompleteLocked(userId); + mUserSetupComplete = Settings.Secure.getIntForUser(mService.mContext.getContentResolver(), + USER_SETUP_COMPLETE, 0, userId) != 0; if ((info.flags & ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS) != 0) { // If the activity itself has requested auto-remove, then just always do it. autoRemoveRecents = true; @@ -1570,6 +1572,7 @@ final class TaskRecord { pw.print(prefix); pw.print("userId="); pw.print(userId); pw.print(" effectiveUid="); UserHandle.formatUid(pw, effectiveUid); pw.print(" mCallingUid="); UserHandle.formatUid(pw, mCallingUid); + pw.print(" mUserSetupComplete="); pw.print(mUserSetupComplete); pw.print(" mCallingPackage="); pw.println(mCallingPackage); if (affinity != null || rootAffinity != null) { pw.print(prefix); pw.print("affinity="); pw.print(affinity); diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index addffd33d746..4a5df7a42f25 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -24,7 +24,6 @@ import static android.app.ActivityManager.USER_OP_IS_CURRENT; import static android.app.ActivityManager.USER_OP_SUCCESS; import static android.content.Context.KEYGUARD_SERVICE; import static android.os.Process.SYSTEM_UID; -import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -46,14 +45,11 @@ import android.app.Dialog; import android.app.IStopUserCallback; import android.app.IUserSwitchObserver; import android.app.KeyguardManager; -import android.content.ContentResolver; import android.content.Context; import android.content.IIntentReceiver; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.UserInfo; -import android.database.ContentObserver; -import android.net.Uri; import android.os.BatteryStats; import android.os.Binder; import android.os.Bundle; @@ -71,12 +67,10 @@ import android.os.UserManager; import android.os.UserManagerInternal; import android.os.storage.IMountService; import android.os.storage.StorageManager; -import android.provider.Settings; import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.android.internal.R; @@ -153,34 +147,6 @@ final class UserController { private final LockPatternUtils mLockPatternUtils; - // Set of users who have completed the set-up process. - private final SparseBooleanArray mSetupCompletedUsers = new SparseBooleanArray(); - private final UserSetupCompleteContentObserver mUserSetupCompleteContentObserver; - - private class UserSetupCompleteContentObserver extends ContentObserver { - private final Uri mUserSetupComplete = Settings.Secure.getUriFor(USER_SETUP_COMPLETE); - - public UserSetupCompleteContentObserver(Handler handler) { - super(handler); - } - - void register(ContentResolver resolver) { - resolver.registerContentObserver(mUserSetupComplete, false, this, UserHandle.USER_ALL); - synchronized (mService) { - updateUserSetupCompleteLocked(UserHandle.USER_ALL); - } - } - - @Override - public void onChange(boolean selfChange, Uri uri, int userId) { - if (mUserSetupComplete.equals(uri)) { - synchronized (mService) { - updateUserSetupCompleteLocked(userId); - } - } - } - } - UserController(ActivityManagerService service) { mService = service; mHandler = mService.mHandler; @@ -190,7 +156,6 @@ final class UserController { mUserLru.add(UserHandle.USER_SYSTEM); mLockPatternUtils = new LockPatternUtils(mService.mContext); updateStartedUserArrayLocked(); - mUserSetupCompleteContentObserver = new UserSetupCompleteContentObserver(mHandler); } void finishUserSwitch(UserState uss) { @@ -477,7 +442,6 @@ final class UserController { mStartedUsers.remove(userId); mUserLru.remove(Integer.valueOf(userId)); updateStartedUserArrayLocked(); - mSetupCompletedUsers.delete(userId); mService.onUserStoppedLocked(userId); // Clean up all state and processes associated with the user. @@ -677,7 +641,6 @@ final class UserController { final Integer userIdInt = userId; mUserLru.remove(userIdInt); mUserLru.add(userIdInt); - updateUserSetupCompleteLocked(userId); if (foreground) { mCurrentUserId = userId; @@ -892,22 +855,6 @@ final class UserController { mUserSwitchObservers.finishBroadcast(); } - void updateUserSetupCompleteLocked(int userId) { - final ContentResolver cr = mService.mContext.getContentResolver(); - for (int i = mStartedUsers.size() - 1; i >= 0; i--) { - int startedUser = mStartedUsers.keyAt(i); - if (startedUser == userId || userId == UserHandle.USER_ALL) { - final boolean setupComplete = - Settings.Secure.getIntForUser(cr, USER_SETUP_COMPLETE, 0, startedUser) != 0; - mSetupCompletedUsers.put(startedUser, setupComplete); - } - } - } - - boolean isUserSetupCompleteLocked(int userId) { - return mSetupCompletedUsers.get(userId); - } - private void stopBackgroundUsersIfEnforced(int oldUserId) { // Never stop system user if (oldUserId == UserHandle.USER_SYSTEM) { @@ -1218,7 +1165,6 @@ final class UserController { void onSystemReady() { updateCurrentProfileIdsLocked(); - mUserSetupCompleteContentObserver.register(mService.mContext.getContentResolver()); } /** diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index fba7e7d76ac3..d2b77aab70b8 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -47,14 +47,15 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.util.Slog; import android.util.SparseArray; - import android.util.SparseIntArray; import android.util.TimeUtils; + import com.android.internal.app.IBatteryStats; import com.android.internal.util.ArrayUtils; import com.android.internal.app.ProcessStats; @@ -1337,8 +1338,48 @@ public final class JobSchedulerService extends com.android.server.SystemService Binder.restoreCallingIdentity(identityToken); } } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + (new JobSchedulerShellCommand(JobSchedulerService.this)).exec( + this, in, out, err, args, resultReceiver); + } }; + // Shell command infrastructure: run the given job immediately + int executeRunCommand(String pkgName, int userId, int jobId, boolean force) { + if (DEBUG) { + Slog.v(TAG, "executeRunCommand(): " + pkgName + "/" + userId + + " " + jobId + " f=" + force); + } + + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, userId); + if (uid < 0) { + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (js == null) { + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + js.overrideState = (force) ? JobStatus.OVERRIDE_FULL : JobStatus.OVERRIDE_SOFT; + if (!js.isConstraintsSatisfied()) { + js.overrideState = 0; + return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + } + + mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + private String printContextIdToJobMap(JobStatus[] map, String initial) { StringBuilder s = new StringBuilder(initial + ": "); for (int i=0; i<map.length; i++) { diff --git a/services/core/java/com/android/server/job/JobSchedulerShellCommand.java b/services/core/java/com/android/server/job/JobSchedulerShellCommand.java new file mode 100644 index 000000000000..2d62c1c4e828 --- /dev/null +++ b/services/core/java/com/android/server/job/JobSchedulerShellCommand.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.app.AppGlobals; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ShellCommand; +import android.os.UserHandle; + +import java.io.PrintWriter; + +public class JobSchedulerShellCommand extends ShellCommand { + public static final int CMD_ERR_NO_PACKAGE = -1000; + public static final int CMD_ERR_NO_JOB = -1001; + public static final int CMD_ERR_CONSTRAINTS = -1002; + + JobSchedulerService mInternal; + IPackageManager mPM; + + JobSchedulerShellCommand(JobSchedulerService service) { + mInternal = service; + mPM = AppGlobals.getPackageManager(); + } + + @Override + public int onCommand(String cmd) { + final PrintWriter pw = getOutPrintWriter(); + try { + if ("run".equals(cmd)) { + return runJob(); + } else { + return handleDefaultCommands(cmd); + } + } catch (Exception e) { + pw.println("Exception: " + e); + } + return -1; + } + + private int runJob() { + try { + final int uid = Binder.getCallingUid(); + final int perm = mPM.checkUidPermission( + "android.permission.CHANGE_APP_IDLE_STATE", uid); + if (perm != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Uid " + uid + + " not permitted to force scheduled jobs"); + } + } catch (RemoteException e) { + // Can't happen + } + + final PrintWriter pw = getOutPrintWriter(); + boolean force = false; + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-f": + case "--force": + force = true; + break; + + case "-u": + case "--user": + userId = Integer.parseInt(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + final String pkgName = getNextArgRequired(); + final int jobId = Integer.parseInt(getNextArgRequired()); + + int ret = mInternal.executeRunCommand(pkgName, userId, jobId, force); + switch (ret) { + case CMD_ERR_NO_PACKAGE: + pw.print("Package not found: "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + break; + + case CMD_ERR_NO_JOB: + pw.print("Could not find job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + break; + + case CMD_ERR_CONSTRAINTS: + pw.print("Job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.print(userId); + pw.println(" has functional constraints but --force not specified"); + break; + + default: + // success! + pw.print("Running job"); + if (force) { + pw.print(" [FORCED]"); + } + pw.println(); + break; + } + return ret; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + + pw.println("Job scheduler (jobscheduler) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(); + pw.println(" run [-f | --force] [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Trigger immediate execution of a specific scheduled job."); + pw.println(" Options:"); + pw.println(" -f or --force: run the job even if technical constraints such as"); + pw.println(" connectivity are not currently met"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(); + } + +} diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index 98bf8a9a8d08..4a2c88c2f733 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -55,6 +55,11 @@ public final class JobStatus { static final int CONSTRAINT_APP_NOT_IDLE = 1<<6; static final int CONSTRAINT_CONTENT_TRIGGER = 1<<7; + // Soft override: ignore constraints like time that don't affect API availability + public static final int OVERRIDE_SOFT = 1; + // Full override: ignore all constraints including API-affecting like connectivity + public static final int OVERRIDE_FULL = 2; + final JobInfo job; /** Uid of the package requesting this job. */ final int callingUid; @@ -91,6 +96,9 @@ public final class JobStatus { public int lastEvaluatedPriority; + // Used by shell commands + public int overrideState = 0; + /** * For use only by ContentObserverController: state it is maintaining about content URIs * being observed. @@ -370,7 +378,7 @@ public final class JobStatus { */ public boolean isReady() { // Deadline constraint trumps other constraints (except for periodic jobs where deadline - // (is an implementation detail. A periodic job should only run if it's constraints are + // is an implementation detail. A periodic job should only run if its constraints are // satisfied). // AppNotIdle implicit constraint trumps all! return (isConstraintsSatisfied() @@ -384,12 +392,27 @@ public final class JobStatus { CONSTRAINT_CONNECTIVITY | CONSTRAINT_UNMETERED | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER; + // Soft override covers all non-"functional" constraints + static final int SOFT_OVERRIDE_CONSTRAINTS = + CONSTRAINT_CHARGING | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE; + /** * @return Whether the constraints set on this job are satisfied. */ public boolean isConstraintsSatisfied() { + if (overrideState == OVERRIDE_FULL) { + // force override: the job is always runnable + return true; + } + final int req = requiredConstraints & CONSTRAINTS_OF_INTEREST; - final int sat = satisfiedConstraints & CONSTRAINTS_OF_INTEREST; + + int sat = satisfiedConstraints & CONSTRAINTS_OF_INTEREST; + if (overrideState == OVERRIDE_SOFT) { + // override: pretend all 'soft' requirements are satisfied + sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS); + } + return (sat & req) == req; } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 9820a1261684..c19b51f54a31 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import android.app.AutomaticZenRule; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -44,7 +45,6 @@ import java.util.Arrays; public class ConditionProviders extends ManagedServices { private final ArrayList<ConditionRecord> mRecords = new ArrayList<>(); - private final ArrayMap<IBinder, IConditionListener> mListeners = new ArrayMap<>(); private final ArraySet<String> mSystemConditionProviderNames; private final ArraySet<SystemConditionProviderService> mSystemConditionProviders = new ArraySet<>(); @@ -103,12 +103,6 @@ public class ConditionProviders extends ManagedServices { } } } - if (filter == null) { - pw.print(" mListeners("); pw.print(mListeners.size()); pw.println("):"); - for (int i = 0; i < mListeners.size(); i++) { - pw.print(" "); pw.println(mListeners.keyAt(i)); - } - } pw.print(" mSystemConditionProviders: "); pw.println(mSystemConditionProviderNames); for (int i = 0; i < mSystemConditionProviders.size(); i++) { mSystemConditionProviders.valueAt(i).dump(pw, filter); @@ -173,16 +167,12 @@ public class ConditionProviders extends ManagedServices { } } - private Condition[] validateConditions(String pkg, Condition[] conditions) { + private Condition[] removeDuplicateConditions(String pkg, Condition[] conditions) { if (conditions == null || conditions.length == 0) return null; final int N = conditions.length; final ArrayMap<Uri, Condition> valid = new ArrayMap<Uri, Condition>(N); for (int i = 0; i < N; i++) { final Uri id = conditions[i].id; - if (!Condition.isValidId(id, pkg)) { - Slog.w(TAG, "Ignoring condition from " + pkg + " for invalid id: " + id); - continue; - } if (valid.containsKey(id)) { Slog.w(TAG, "Ignoring condition from " + pkg + " for duplicate id: " + id); continue; @@ -219,16 +209,9 @@ public class ConditionProviders extends ManagedServices { synchronized(mMutex) { if (DEBUG) Slog.d(TAG, "notifyConditions pkg=" + pkg + " info=" + info + " conditions=" + (conditions == null ? null : Arrays.asList(conditions))); - conditions = validateConditions(pkg, conditions); + conditions = removeDuplicateConditions(pkg, conditions); if (conditions == null || conditions.length == 0) return; final int N = conditions.length; - for (IConditionListener listener : mListeners.values()) { - try { - listener.onConditionsReceived(conditions); - } catch (RemoteException e) { - Slog.w(TAG, "Error sending conditions to listener " + listener, e); - } - } for (int i = 0; i < N; i++) { final Condition c = conditions[i]; final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/); diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 8d75f603928e..56128708599b 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -16,6 +16,9 @@ package com.android.server.pm; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; @@ -27,8 +30,12 @@ import android.content.pm.IOnAppsChangedListener; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutServiceInternal; +import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; import android.content.pm.UserInfo; import android.graphics.Rect; import android.net.Uri; @@ -44,15 +51,16 @@ import android.util.Log; import android.util.Slog; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; import com.android.server.SystemService; import java.util.List; /** * Service that manages requests and callbacks for launchers that support - * managed profiles. + * managed profiles. */ - public class LauncherAppsService extends SystemService { private final LauncherAppsImpl mLauncherAppsImpl; @@ -67,21 +75,25 @@ public class LauncherAppsService extends SystemService { publishBinderService(Context.LAUNCHER_APPS_SERVICE, mLauncherAppsImpl); } - class LauncherAppsImpl extends ILauncherApps.Stub { + static class LauncherAppsImpl extends ILauncherApps.Stub { private static final boolean DEBUG = false; private static final String TAG = "LauncherAppsService"; private final Context mContext; private final PackageManager mPm; private final UserManager mUm; + private final ShortcutServiceInternal mShortcutServiceInternal; private final PackageCallbackList<IOnAppsChangedListener> mListeners = new PackageCallbackList<IOnAppsChangedListener>(); - private MyPackageMonitor mPackageMonitor = new MyPackageMonitor(); + private final MyPackageMonitor mPackageMonitor = new MyPackageMonitor(); public LauncherAppsImpl(Context context) { mContext = context; mPm = mContext.getPackageManager(); mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mShortcutServiceInternal = Preconditions.checkNotNull( + LocalServices.getService(ShortcutServiceInternal.class)); + mShortcutServiceInternal.addListener(mPackageMonitor); } /* @@ -174,6 +186,20 @@ public class LauncherAppsService extends SystemService { } } + private void verifyCallingPackage(String callingPackage) { + int packageUid = -1; + try { + packageUid = mPm.getPackageUid(callingPackage, + PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + Log.e(TAG, "Package not found: " + callingPackage); + } + if (packageUid != Binder.getCallingUid()) { + throw new SecurityException("Calling package name mismatch"); + } + } + /** * Checks if the user is enabled. */ @@ -264,6 +290,70 @@ public class LauncherAppsService extends SystemService { } } + private void enforceShortcutPermission(UserHandle user) { + ensureInUserProfiles(user, "Cannot start activity for unrelated profile " + user); + // STOPSHIP Implement it + } + + @Override + public ParceledListSlice getShortcuts(String callingPackage, long changedSince, + String packageName, ComponentName componentName, int flags, UserHandle user) + throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + return new ParceledListSlice<>( + mShortcutServiceInternal.getShortcuts(callingPackage, changedSince, packageName, + componentName, flags, user.getIdentifier())); + } + + @Override + public ParceledListSlice getShortcutInfo(String callingPackage, String packageName, + List<String> ids, UserHandle user) throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + return new ParceledListSlice<>( + mShortcutServiceInternal.getShortcutInfo(callingPackage, packageName, + ids, user.getIdentifier())); + } + + @Override + public void pinShortcuts(String callingPackage, String packageName, List<String> ids, + UserHandle user) throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + mShortcutServiceInternal.pinShortcuts(callingPackage, packageName, + ids, user.getIdentifier()); + } + + @Override + public boolean startShortcut(String callingPackage, String packageName, String shortcutId, + Rect sourceBounds, Bundle startActivityOptions, UserHandle user) + throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + final Intent intent = mShortcutServiceInternal.createShortcutIntent(callingPackage, + packageName, shortcutId, user.getIdentifier()); + if (intent == null) { + return false; + } + // Note the target activity doesn't have to be exported. + + intent.setSourceBounds(sourceBounds); + prepareIntentForLaunch(intent, sourceBounds); + + final long ident = Binder.clearCallingIdentity(); + try { + mContext.startActivityAsUser(intent, startActivityOptions, user); + } finally { + Binder.restoreCallingIdentity(ident); + } + return true; + } + @Override public boolean isActivityEnabled(ComponentName component, UserHandle user) throws RemoteException { @@ -293,9 +383,7 @@ public class LauncherAppsService extends SystemService { Intent launchIntent = new Intent(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); - launchIntent.setSourceBounds(sourceBounds); - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + prepareIntentForLaunch(launchIntent, sourceBounds); launchIntent.setPackage(component.getPackageName()); long ident = Binder.clearCallingIdentity(); @@ -332,6 +420,13 @@ public class LauncherAppsService extends SystemService { } } + private void prepareIntentForLaunch(@NonNull Intent launchIntent, + @Nullable Rect sourceBounds) { + launchIntent.setSourceBounds(sourceBounds); + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + @Override public void showAppDetailsAsUser(ComponentName component, Rect sourceBounds, Bundle opts, UserHandle user) throws RemoteException { @@ -355,7 +450,7 @@ public class LauncherAppsService extends SystemService { } - private class MyPackageMonitor extends PackageMonitor { + private class MyPackageMonitor extends PackageMonitor implements ShortcutChangeListener { /** Checks if user is a profile of or same as listeningUser. * and the user is enabled. */ @@ -390,6 +485,8 @@ public class LauncherAppsService extends SystemService { } } + // TODO Simplify with lambdas. + @Override public void onPackageAdded(String packageName, int uid) { UserHandle user = new UserHandle(getChangingUserId()); @@ -523,6 +620,25 @@ public class LauncherAppsService extends SystemService { super.onPackagesUnsuspended(packages); } + @Override + public void onShortcutChanged(@NonNull String packageName, + @NonNull List<ShortcutInfo> shortcuts, @UserIdInt int userId) { + final UserHandle user = UserHandle.of(userId); + + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + IOnAppsChangedListener listener = mListeners.getBroadcastItem(i); + UserHandle listeningUser = (UserHandle) mListeners.getBroadcastCookie(i); + if (!isEnabledProfileOf(user, listeningUser, "onShortcutChanged")) continue; + try { + listener.onShortcutChanged(user, packageName, + new ParceledListSlice<>(shortcuts)); + } catch (RemoteException re) { + Slog.d(TAG, "Callback failed ", re); + } + } + mListeners.finishBroadcast(); + } } class PackageCallbackList<T extends IInterface> extends RemoteCallbackList<T> { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java new file mode 100644 index 000000000000..423767ad87b4 --- /dev/null +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -0,0 +1,1530 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.IShortcutService; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherApps.ShortcutQuery; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutServiceInternal; +import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; +import android.graphics.drawable.Icon; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCommand; +import android.os.UserHandle; +import android.text.TextUtils; +import android.text.format.Time; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; +import com.android.server.SystemService; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * TODO: + * - Make save async + * + * - Add Bitmap support + * + * - Implement updateShortcuts + * + * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res + * + * - Pinned per each launcher package (multiple launchers) + * + * - Dev option to reset all counts for QA (for now use "adb shell cmd shortcut reset-throttling") + * + * - Load config from settings + */ +public class ShortcutService extends IShortcutService.Stub { + private static final String TAG = "ShortcutService"; + + private static final boolean DEBUG = true; // STOPSHIP if true + private static final boolean DEBUG_LOAD = true; // STOPSHIP if true + + private static final int DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day + private static final int DEFAULT_MAX_DAILY_UPDATES = 10; + private static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5; + + private static final int SAVE_DELAY_MS = 5000; // in milliseconds. + + @VisibleForTesting + static final String FILENAME_BASE_STATE = "shortcut_service.xml"; + + @VisibleForTesting + static final String DIRECTORY_PER_USER = "shortcut_service"; + + @VisibleForTesting + static final String FILENAME_USER_PACKAGES = "shortcuts.xml"; + + private static final String DIRECTORY_BITMAPS = "bitmaps"; + + private static final String TAG_ROOT = "root"; + private static final String TAG_LAST_RESET_TIME = "last_reset_time"; + private static final String ATTR_VALUE = "value"; + + private final Context mContext; + + private final Object mLock = new Object(); + + private final Handler mHandler; + + @GuardedBy("mLock") + private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1); + + @GuardedBy("mLock") + private long mRawLastResetTime; + + /** + * All the information relevant to shortcuts from a single package (per-user). + * + * TODO Move the persisting code to this class. + */ + private static class PackageShortcuts { + /** + * All the shortcuts from the package, keyed on IDs. + */ + final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); + + /** + * # of dynamic shortcuts. + */ + private int mDynamicShortcutCount = 0; + + /** + * # of times the package has called rate-limited APIs. + */ + private int mApiCallCountInner; + + /** + * When {@link #mApiCallCountInner} was reset last time. + */ + private long mLastResetTime; + + /** + * @return the all shortcuts. Note DO NOT add/remove or touch the flags of the result + * directly, which would cause {@link #mDynamicShortcutCount} to be out of sync. + */ + @GuardedBy("mLock") + public ArrayMap<String, ShortcutInfo> getShortcuts() { + return mShortcuts; + } + + /** + * Add a shortcut, or update one with the same ID, with taking over existing flags. + * + * It checks the max number of dynamic shortcuts. + */ + @GuardedBy("mLock") + public void updateShortcutWithCapping(@NonNull ShortcutService s, + @NonNull ShortcutInfo newShortcut) { + final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); + + int oldFlags = 0; + int newDynamicCount = mDynamicShortcutCount; + + if (oldShortcut != null) { + oldFlags = oldShortcut.getFlags(); + if (oldShortcut.isDynamic()) { + newDynamicCount--; + } + } + if (newShortcut.isDynamic()) { + newDynamicCount++; + } + // Make sure there's still room. + s.enforceMaxDynamicShortcuts(newDynamicCount); + + // Okay, make it dynamic and add. + newShortcut.addFlags(oldFlags); + + mShortcuts.put(newShortcut.getId(), newShortcut); + mDynamicShortcutCount = newDynamicCount; + } + + @GuardedBy("mLock") + public void deleteAllDynamicShortcuts() { + ArrayList<String> removeList = null; // Lazily initialize. + + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + + if (!si.isDynamic()) { + continue; + } + if (si.isPinned()) { + // Still pinned, so don't remove; just make it non-dynamic. + si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + } else { + if (removeList == null) { + removeList = new ArrayList<>(); + } + removeList.add(si.getId()); + } + } + if (removeList != null) { + for (int i = removeList.size() - 1 ; i >= 0; i--) { + mShortcuts.remove(removeList.get(i)); + } + } + mDynamicShortcutCount = 0; + } + + @GuardedBy("mLock") + public void deleteDynamicWithId(@NonNull String shortcutId) { + final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); + + if (oldShortcut == null) { + return; + } + if (oldShortcut.isDynamic()) { + mDynamicShortcutCount--; + } + if (oldShortcut.isPinned()) { + oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + } else { + mShortcuts.remove(shortcutId); + } + } + + @GuardedBy("mLock") + public void pinAll(List<String> shortcutIds) { + for (int i = shortcutIds.size() - 1; i >= 0; i--) { + final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i)); + if (shortcut != null) { + shortcut.addFlags(ShortcutInfo.FLAG_PINNED); + } + } + } + + /** + * Number of calls that the caller has made, since the last reset. + */ + @GuardedBy("mLock") + public int getApiCallCount(@NonNull ShortcutService s) { + final long last = s.getLastResetTimeLocked(); + + // If not reset yet, then reset. + if (mLastResetTime < last) { + mApiCallCountInner = 0; + mLastResetTime = last; + } + return mApiCallCountInner; + } + + /** + * If the caller app hasn't been throttled yet, increment {@link #mApiCallCountInner} + * and return true. Otherwise just return false. + */ + @GuardedBy("mLock") + public boolean tryApiCall(@NonNull ShortcutService s) { + if (getApiCallCount(s) >= s.mMaxDailyUpdates) { + return false; + } + mApiCallCountInner++; + return true; + } + + @GuardedBy("mLock") + public void resetRateLimitingForCommandLine() { + mApiCallCountInner = 0; + mLastResetTime = 0; + } + + /** + * Find all shortcuts that match {@code query}. + */ + @GuardedBy("mLock") + public void findAll(@NonNull List<ShortcutInfo> result, + @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { + for (int i = 0; i < mShortcuts.size(); i++) { + final ShortcutInfo si = mShortcuts.valueAt(i); + if (query == null || query.test(si)) { + result.add(si.clone(cloneFlag)); + } + } + } + } + + /** + * User ID -> package name -> list of ShortcutInfos. + */ + @GuardedBy("mLock") + private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts = + new SparseArray<>(); + + /** + * Max number of dynamic shortcuts that each application can have at a time. + */ + @GuardedBy("mLock") + private int mMaxDynamicShortcuts; + + /** + * Max number of updating API calls that each application can make a day. + */ + @GuardedBy("mLock") + private int mMaxDailyUpdates; + + /** + * Actual throttling-reset interval. By default it's a day. + */ + @GuardedBy("mLock") + private long mResetInterval; + + public ShortcutService(Context context) { + mContext = Preconditions.checkNotNull(context); + LocalServices.addService(ShortcutServiceInternal.class, new LocalService()); + mHandler = new Handler(BackgroundThread.get().getLooper()); + } + + /** + * System service lifecycle. + */ + public static final class Lifecycle extends SystemService { + final ShortcutService mService; + + public Lifecycle(Context context) { + super(context); + mService = new ShortcutService(context); + } + + @Override + public void onStart() { + publishBinderService(Context.SHORTCUT_SERVICE, mService); + } + + @Override + public void onBootPhase(int phase) { + mService.onBootPhase(phase); + } + + @Override + public void onCleanupUser(int userHandle) { + synchronized (mService.mLock) { + mService.onCleanupUserInner(userHandle); + } + } + + @Override + public void onStartUser(int userId) { + synchronized (mService.mLock) { + mService.onStartUserLocked(userId); + } + } + } + + /** lifecycle event */ + void onBootPhase(int phase) { + if (DEBUG) { + Slog.d(TAG, "onBootPhase: " + phase); + } + switch (phase) { + case SystemService.PHASE_LOCK_SETTINGS_READY: + initialize(); + break; + } + } + + /** lifecycle event */ + void onStartUserLocked(int userId) { + // Preload + getUserShortcutsLocked(userId); + } + + /** lifecycle event */ + void onCleanupUserInner(int userId) { + // Unload + mShortcuts.delete(userId); + } + + /** Return the base state file name */ + private AtomicFile getBaseStateFile() { + final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE); + path.mkdirs(); + return new AtomicFile(path); + } + + /** + * Init the instance. (load the state file, etc) + */ + private void initialize() { + synchronized (mLock) { + injectLoadConfigurationLocked(); + loadBaseStateLocked(); + } + } + + // Test overrides it to inject different values. + @VisibleForTesting + void injectLoadConfigurationLocked() { + mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L; + mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES; + mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP; + } + + // === Persistings === + + @Nullable + private String parseStringAttribute(XmlPullParser parser, String attribute) { + return parser.getAttributeValue(null, attribute); + } + + private long parseLongAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return 0; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing long " + value); + return 0; + } + } + + @Nullable + private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return null; + } + return ComponentName.unflattenFromString(value); + } + + @Nullable + private Intent parseIntentAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return null; + } + try { + return Intent.parseUri(value, /* flags =*/ 0); + } catch (URISyntaxException e) { + Slog.e(TAG, "Error parsing intent", e); + return null; + } + } + + private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException { + if (TextUtils.isEmpty(value)) return; + + out.startTag(null, tag); + out.attribute(null, ATTR_VALUE, value); + out.endTag(null, tag); + } + + private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException { + writeTagValue(out, tag, Long.toString(value)); + } + + private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle) + throws IOException, XmlPullParserException { + if (bundle == null) return; + + out.startTag(null, tag); + bundle.saveToXml(out); + out.endTag(null, tag); + } + + private void writeAttr(XmlSerializer out, String name, String value) throws IOException { + if (TextUtils.isEmpty(value)) return; + + out.attribute(null, name, value); + } + + private void writeAttr(XmlSerializer out, String name, long value) throws IOException { + writeAttr(out, name, String.valueOf(value)); + } + + private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException { + if (comp == null) return; + writeAttr(out, name, comp.flattenToString()); + } + + private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException { + if (intent == null) return; + + writeAttr(out, name, intent.toUri(/* flags =*/ 0)); + } + + @VisibleForTesting + void saveBaseStateLocked() { + final AtomicFile file = getBaseStateFile(); + if (DEBUG) { + Slog.i(TAG, "Saving to " + file.getBaseFile()); + } + + FileOutputStream outs = null; + try { + outs = file.startWrite(); + + // Write to XML + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outs, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + + // Body. + writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime); + + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); + + // Close. + file.finishWrite(outs); + } catch (IOException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } + } + + private void loadBaseStateLocked() { + mRawLastResetTime = 0; + + final AtomicFile file = getBaseStateFile(); + if (DEBUG) { + Slog.i(TAG, "Loading from " + file.getBaseFile()); + } + try (FileInputStream in = file.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, StandardCharsets.UTF_8.name()); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + // Check the root tag + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_ROOT.equals(tag)) { + Slog.e(TAG, "Invalid root tag: " + tag); + return; + } + continue; + } + // Assume depth == 2 + switch (tag) { + case TAG_LAST_RESET_TIME: + mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE); + break; + default: + Slog.e(TAG, "Invalid tag: " + tag); + break; + } + } + } catch (FileNotFoundException e) { + // Use the default + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); + + mRawLastResetTime = 0; + } + // Adjust the last reset time. + getLastResetTimeLocked(); + } + + private void saveUserLocked(@UserIdInt int userId) { + final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); + if (DEBUG) { + Slog.i(TAG, "Saving to " + path); + } + path.mkdirs(); + final AtomicFile file = new AtomicFile(path); + FileOutputStream outs = null; + try { + outs = file.startWrite(); + + // Write to XML + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outs, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + + final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId); + + // Body. + for (int i = 0; i < packages.size(); i++) { + final String packageName = packages.keyAt(i); + final PackageShortcuts shortcuts = packages.valueAt(i); + + // TODO Move this to PackageShortcuts. + + out.startTag(null, "package"); + + writeAttr(out, "name", packageName); + writeAttr(out, "dynamic-count", shortcuts.mDynamicShortcutCount); + writeAttr(out, "call-count", shortcuts.mApiCallCountInner); + writeAttr(out, "last-reset", shortcuts.mLastResetTime); + + final int size = shortcuts.getShortcuts().size(); + for (int j = 0; j < size; j++) { + saveShortcut(out, shortcuts.getShortcuts().valueAt(j)); + } + + out.endTag(null, "package"); + } + + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); + + // Close. + file.finishWrite(outs); + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } + } + + private void saveShortcut(XmlSerializer out, ShortcutInfo si) + throws IOException, XmlPullParserException { + out.startTag(null, "shortcut"); + writeAttr(out, "id", si.getId()); + // writeAttr(out, "package", si.getPackageName()); // not needed + writeAttr(out, "activity", si.getActivityComponent()); + // writeAttr(out, "icon", si.getIcon()); // We don't save it. + writeAttr(out, "title", si.getTitle()); + writeAttr(out, "intent", si.getIntent()); + writeAttr(out, "weight", si.getWeight()); + writeAttr(out, "timestamp", si.getLastChangedTimestamp()); + writeAttr(out, "flags", si.getFlags()); + writeAttr(out, "icon-res", si.getIconResourceId()); + writeAttr(out, "bitmap-path", si.getBitmapPath()); + + writeTagExtra(out, "intent-extras", si.getIntentPersistableExtras()); + writeTagExtra(out, "extras", si.getExtras()); + + out.endTag(null, "shortcut"); + } + + private static IOException throwForInvalidTag(int depth, String tag) throws IOException { + throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth)); + } + + @Nullable + private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) { + final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); + if (DEBUG) { + Slog.i(TAG, "Loading from " + path); + } + path.mkdirs(); + final AtomicFile file = new AtomicFile(path); + + final FileInputStream in; + try { + in = file.openRead(); + } catch (FileNotFoundException e) { + if (DEBUG) { + Slog.i(TAG, "Not found " + path); + } + return null; + } + final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>(); + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, StandardCharsets.UTF_8.name()); + + String packageName = null; + PackageShortcuts shortcuts = null; + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + + // TODO Move some of this to PackageShortcuts. + + final String tag = parser.getName(); + if (DEBUG_LOAD) { + Slog.d(TAG, String.format("depth=%d type=%d name=%s", + depth, type, tag)); + } + switch (depth) { + case 1: { + if (TAG_ROOT.equals(tag)) { + continue; + } + break; + } + case 2: { + switch (tag) { + case "package": + packageName = parseStringAttribute(parser, "name"); + shortcuts = new PackageShortcuts(); + ret.put(packageName, shortcuts); + + shortcuts.mDynamicShortcutCount = + (int) parseLongAttribute(parser, "dynamic-count"); + shortcuts.mApiCallCountInner = + (int) parseLongAttribute(parser, "call-count"); + shortcuts.mLastResetTime = parseLongAttribute(parser, "last-reset"); + continue; + } + break; + } + case 3: { + switch (tag) { + case "shortcut": + final ShortcutInfo si = parseShortcut(parser, packageName); + shortcuts.mShortcuts.put(si.getId(), si); + continue; + } + break; + } + } + throwForInvalidTag(depth, tag); + } + return ret; + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); + return null; + } finally { + IoUtils.closeQuietly(in); + } + } + + private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName) + throws IOException, XmlPullParserException { + String id; + ComponentName activityComponent; + Icon icon; + String title; + Intent intent; + PersistableBundle intentPersistableExtras = null; + int weight; + PersistableBundle extras = null; + long lastChangedTimestamp; + int flags; + int iconRes; + String bitmapPath; + + id = parseStringAttribute(parser, "id"); + activityComponent = parseComponentNameAttribute(parser, "activity"); + title = parseStringAttribute(parser, "title"); + intent = parseIntentAttribute(parser, "intent"); + weight = (int) parseLongAttribute(parser, "weight"); + lastChangedTimestamp = (int) parseLongAttribute(parser, "timestamp"); + flags = (int) parseLongAttribute(parser, "flags"); + iconRes = (int) parseLongAttribute(parser, "icon-res"); + bitmapPath = parseStringAttribute(parser, "bitmap-path"); + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + final String tag = parser.getName(); + if (DEBUG_LOAD) { + Slog.d(TAG, String.format(" depth=%d type=%d name=%s", + depth, type, tag)); + } + switch (tag) { + case "intent-extras": + intentPersistableExtras = PersistableBundle.restoreFromXml(parser); + continue; + case "extras": + extras = PersistableBundle.restoreFromXml(parser); + continue; + } + throw throwForInvalidTag(depth, tag); + } + return new ShortcutInfo( + id, packgeName, activityComponent, /* icon =*/ null, title, intent, + intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, + iconRes, bitmapPath); + } + + // TODO Actually make it async. + private void scheduleSaveBaseState() { + synchronized (mLock) { + saveBaseStateLocked(); + } + } + + // TODO Actually make it async. + private void scheduleSaveUser(@UserIdInt int userId) { + synchronized (mLock) { + saveUserLocked(userId); + } + } + + /** Return the last reset time. */ + long getLastResetTimeLocked() { + updateTimes(); + return mRawLastResetTime; + } + + /** Return the next reset time. */ + long getNextResetTimeLocked() { + updateTimes(); + return mRawLastResetTime + mResetInterval; + } + + /** + * Update the last reset time. + */ + private void updateTimes() { + + final long now = injectCurrentTimeMillis(); + + final long prevLastResetTime = mRawLastResetTime; + + if (mRawLastResetTime == 0) { // first launch. + // TODO Randomize?? + mRawLastResetTime = now; + } else if (now < mRawLastResetTime) { + // Clock rewound. + // TODO Randomize?? + mRawLastResetTime = now; + } else { + // TODO Do it properly. + while ((mRawLastResetTime + mResetInterval) <= now) { + mRawLastResetTime += mResetInterval; + } + } + if (prevLastResetTime != mRawLastResetTime) { + scheduleSaveBaseState(); + } + } + + /** Return the per-user state. */ + @GuardedBy("mLock") + @NonNull + private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) { + ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId); + if (userPackages == null) { + userPackages = loadUserLocked(userId); + if (userPackages == null) { + userPackages = new ArrayMap<>(); + } + mShortcuts.put(userId, userPackages); + } + return userPackages; + } + + /** Return the per-user per-package state. */ + @GuardedBy("mLock") + @NonNull + private PackageShortcuts getPackageShortcutsLocked( + @NonNull String packageName, @UserIdInt int userId) { + final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId); + PackageShortcuts shortcuts = userPackages.get(packageName); + if (shortcuts == null) { + shortcuts = new PackageShortcuts(); + userPackages.put(packageName, shortcuts); + } + return shortcuts; + } + + // === Caller validation === + + private boolean isCallerSystem() { + final int callingUid = injectBinderCallingUid(); + return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); + } + + private boolean isCallerShell() { + final int callingUid = injectBinderCallingUid(); + return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; + } + + private void enforceSystemOrShell() { + Preconditions.checkState(isCallerSystem() || isCallerShell(), + "Caller must be system or shell"); + } + + private void enforceShell() { + Preconditions.checkState(isCallerShell(), "Caller must be shell"); + } + + private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { + Preconditions.checkStringNotEmpty(packageName, "packageName"); + + if (isCallerSystem()) { + return; // no check + } + + final int callingUid = injectBinderCallingUid(); + + // Otherwise, make sure the arguments are valid. + if (UserHandle.getUserId(callingUid) != userId) { + throw new SecurityException("Invalid user-ID"); + } + verifyCallingPackage(packageName); + } + + private void verifyCallingPackage(@NonNull String packageName) { + Preconditions.checkStringNotEmpty(packageName, "packageName"); + + if (isCallerSystem()) { + return; // no check + } + + if (injectGetPackageUid(packageName) == injectBinderCallingUid()) { + return; // Caller is valid. + } + throw new SecurityException("Caller UID= doesn't own " + packageName); + } + + // Test overrides it. + int injectGetPackageUid(String packageName) { + try { + + // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? + + return mContext.getPackageManager().getPackageUid(packageName, + PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + return -1; + } + } + + /** + * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. + */ + void enforceMaxDynamicShortcuts(int numShortcuts) { + if (numShortcuts > mMaxDynamicShortcuts) { + throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); + } + } + + /** + * - Sends a notification to LauncherApps + * - Write to file + */ + private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { + notifyListeners(packageName, userId); + scheduleSaveUser(userId); + } + + private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { + final ArrayList<ShortcutChangeListener> copy; + final List<ShortcutInfo> shortcuts = new ArrayList<>(); + synchronized (mLock) { + copy = new ArrayList<>(mListeners); + + getPackageShortcutsLocked(packageName, userId) + .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); + } + for (int i = copy.size() - 1; i >= 0; i--) { + copy.get(i).onShortcutChanged(packageName, shortcuts, userId); + } + } + + /** + * Clean up / validate an incoming shortcut. + * - Make sure all mandatory fields are set. + * - Make sure the intent's extras are persistable, and them to set + * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. + * - Clear flags. + */ + private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut) { + Preconditions.checkNotNull(shortcut, "Null shortcut detected"); + if (shortcut.getActivityComponent() != null) { + Preconditions.checkState( + shortcut.getPackageName().equals( + shortcut.getActivityComponent().getPackageName()), + "Activity package name mismatch"); + } + + shortcut.enforceMandatoryFields(); + + final Intent intent = shortcut.getIntent(); + final Bundle intentExtras = intent.getExtras(); + if (intentExtras != null && intentExtras.size() > 0) { + intent.replaceExtras((Bundle) null); + + // PersistableBundle's constructor will throw IllegalArgumentException if original + // extras contain something not persistable. + shortcut.setIntentPersistableExtras(new PersistableBundle(intentExtras)); + } + + // TODO Save the icon + shortcut.setIcon(null); + + shortcut.setFlags(0); + } + + // === APIs === + + @Override + public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); + final int size = newShortcuts.size(); + + synchronized (mLock) { + final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); + + // Throttling. + if (!ps.tryApiCall(this)) { + return false; + } + enforceMaxDynamicShortcuts(size); + + // Validate the shortcuts. + for (int i = 0; i < size; i++) { + fixUpIncomingShortcutInfo(newShortcuts.get(i)); + } + + // First, remove all un-pinned; dynamic shortcuts + ps.deleteAllDynamicShortcuts(); + + // Then, add/update all. We need to make sure to take over "pinned" flag. + for (int i = 0; i < size; i++) { + final ShortcutInfo newShortcut = newShortcuts.get(i); + newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + ps.updateShortcutWithCapping(this, newShortcut); + } + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); + + synchronized (mLock) { + + if (true) { + throw new RuntimeException("not implemented yet"); + } + + // TODO Similar to setDynamicShortcuts, but don't add new ones, and don't change flags. + // Update non-null fields only. + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); + + // Throttling. + if (!ps.tryApiCall(this)) { + return false; + } + + // Validate the shortcut. + fixUpIncomingShortcutInfo(newShortcut); + + // Add it. + newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + ps.updateShortcutWithCapping(this, newShortcut); + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public void deleteDynamicShortcut(String packageName, String shortcutId, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(shortcutId); + } + userPackageChanged(packageName, userId); + } + + @Override + public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(); + } + userPackageChanged(packageName, userId); + } + + @Override + public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + synchronized (mLock) { + return getShortcutsWithQueryLocked( + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, + ShortcutInfo::isDynamic); + } + } + + @Override + public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + synchronized (mLock) { + return getShortcutsWithQueryLocked( + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, + ShortcutInfo::isPinned); + } + } + + private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, + @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) { + + final ArrayList<ShortcutInfo> ret = new ArrayList<>(); + + getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); + + return new ParceledListSlice<>(ret); + } + + @Override + public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) + throws RemoteException { + verifyCaller(packageName, userId); + + return mMaxDynamicShortcuts; + } + + @Override + public int getRemainingCallCount(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + return mMaxDailyUpdates + - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); + } + } + + @Override + public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + return getNextResetTimeLocked(); + } + } + + /** + * Reset all throttling, for developer options and command line. Only system/shell can call it. + */ + @Override + public void resetThrottling() { + enforceSystemOrShell(); + + resetThrottlingInner(); + } + + @VisibleForTesting + void resetThrottlingInner() { + synchronized (mLock) { + mRawLastResetTime = injectCurrentTimeMillis(); + } + scheduleSaveBaseState(); + } + + /** + * Entry point from {@link LauncherApps}. + */ + private class LocalService extends ShortcutServiceInternal { + @Override + public List<ShortcutInfo> getShortcuts( + @NonNull String callingPackage, long changedSince, + @Nullable String packageName, @Nullable ComponentName componentName, + int queryFlags, int userId) { + final ArrayList<ShortcutInfo> ret = new ArrayList<>(); + final int cloneFlag = + ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) + ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER + : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; + + synchronized (mLock) { + if (packageName != null) { + getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, + userId, ret, cloneFlag); + } else { + final ArrayMap<String, PackageShortcuts> packages = + getUserShortcutsLocked(userId); + for (int i = 0; i < packages.size(); i++) { + getShortcutsInnerLocked( + packages.keyAt(i), + changedSince, componentName, queryFlags, userId, ret, cloneFlag); + } + } + } + return ret; + } + + private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, + @Nullable ComponentName componentName, int queryFlags, + int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) { + getPackageShortcutsLocked(packageName, userId).findAll(ret, + (ShortcutInfo si) -> { + if (si.getLastChangedTimestamp() < changedSince) { + return false; + } + if (componentName != null + && !componentName.equals(si.getActivityComponent())) { + return false; + } + final boolean matchDynamic = + ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) + && si.isDynamic(); + final boolean matchPinned = + ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) + && si.isPinned(); + return matchDynamic || matchPinned; + }, cloneFlag); + } + + @Override + public List<ShortcutInfo> getShortcutInfo( + @NonNull String callingPackage, + @NonNull String packageName, @Nullable List<String> ids, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkStringNotEmpty(packageName, "packageName"); + + final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size()); + final ArraySet<String> idSet = new ArraySet<>(ids); + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).findAll(ret, + (ShortcutInfo si) -> idSet.contains(si.getId()), + ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); + } + return ret; + } + + @Override + public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, + @NonNull List<String> shortcutIds, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkStringNotEmpty(packageName, "packageName"); + Preconditions.checkNotNull(shortcutIds, "shortcutIds"); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).pinAll(shortcutIds); + } + userPackageChanged(packageName, userId); + } + + @Override + public Intent createShortcutIntent(@NonNull String callingPackage, + @NonNull String packageName, @NonNull String shortcutId, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty"); + Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); + + synchronized (mLock) { + final ShortcutInfo fullShortcut = + getPackageShortcutsLocked(packageName, userId) + .getShortcuts().get(shortcutId); + if (fullShortcut == null) { + return null; + } else { + final Intent intent = fullShortcut.getIntent(); + final PersistableBundle extras = fullShortcut.getIntentPersistableExtras(); + if (extras != null) { + intent.replaceExtras(new Bundle(extras)); + } + + return intent; + } + } + } + + @Override + public void addListener(@NonNull ShortcutChangeListener listener) { + synchronized (mLock) { + mListeners.add(Preconditions.checkNotNull(listener)); + } + } + } + + // === Dump === + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump UserManager from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " without permission " + + android.Manifest.permission.DUMP); + return; + } + dumpInner(pw); + } + + @VisibleForTesting + void dumpInner(PrintWriter pw) { + synchronized (mLock) { + final long now = injectCurrentTimeMillis(); + pw.print("Now: ["); + pw.print(now); + pw.print("] "); + pw.print(formatTime(now)); + pw.print(" Raw last reset: ["); + pw.print(mRawLastResetTime); + pw.print("] "); + pw.print(formatTime(mRawLastResetTime)); + + final long last = getLastResetTimeLocked(); + final long next = getNextResetTimeLocked(); + pw.print(" Last reset: ["); + pw.print(last); + pw.print("] "); + pw.print(formatTime(last)); + + pw.print(" Next reset: ["); + pw.print(next); + pw.print("] "); + pw.print(formatTime(next)); + pw.println(); + + pw.println(); + + for (int i = 0; i < mShortcuts.size(); i++) { + dumpUserLocked(pw, mShortcuts.keyAt(i)); + } + + } + } + + private void dumpUserLocked(PrintWriter pw, int userId) { + pw.print(" User: "); + pw.print(userId); + pw.println(); + + final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); + if (packages == null) { + return; + } + for (int j = 0; j < packages.size(); j++) { + dumpPackageLocked(pw, userId, packages.keyAt(j)); + } + pw.println(); + } + + private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { + final PackageShortcuts shortcuts = mShortcuts.get(userId).get(packageName); + if (shortcuts == null) { + return; + } + + pw.print(" Package: "); + pw.print(packageName); + pw.println(); + + pw.print(" Calls: "); + pw.print(shortcuts.getApiCallCount(this)); + pw.println(); + + // This should be after getApiCallCount(), which may update it. + pw.print(" Last reset: ["); + pw.print(shortcuts.mLastResetTime); + pw.print("] "); + pw.print(formatTime(shortcuts.mLastResetTime)); + pw.println(); + + pw.println(" Shortcuts:"); + final int size = shortcuts.getShortcuts().size(); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(shortcuts.getShortcuts().valueAt(i).toInsecureString()); + } + } + + private static String formatTime(long time) { + Time tobj = new Time(); + tobj.set(time); + return tobj.format("%Y-%m-%d %H:%M:%S"); + } + + // === Shell support === + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + + enforceShell(); + + (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); + } + + /** + * Handle "adb shell cmd". + */ + private class MyShellCommand extends ShellCommand { + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + switch(cmd) { + case "reset-package-throttling": + return handleResetPackageThrottling(); + case "reset-throttling": + return handleResetThrottling(); + default: + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("Usage: cmd shortcut COMMAND [options ...]"); + pw.println(); + pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); + pw.println(" Reset throttling for a package"); + pw.println(); + pw.println("cmd shortcut reset-throttling"); + pw.println(" Reset throttling for all packages and users"); + pw.println(); + } + + private int handleResetThrottling() { + resetThrottling(); + return 0; + } + + private int handleResetPackageThrottling() { + final PrintWriter pw = getOutPrintWriter(); + + int userId = UserHandle.USER_SYSTEM; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + default: + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + final String packageName = getNextArgRequired(); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); + saveUserLocked(userId); + } + + return 0; + } + } + + // === Unit test support === + + // Injection point. + long injectCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + // Injection point. + int injectBinderCallingUid() { + return getCallingUid(); + } + + File injectSystemDataPath() { + return Environment.getDataSystemDirectory(); + } + + File injectUserDataPath(@UserIdInt int userId) { + return new File(Environment.getDataSystemDeDirectory(userId), DIRECTORY_PER_USER); + } + + @VisibleForTesting + SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { + return mShortcuts; + } + + @VisibleForTesting + void setMaxDynamicShortcutsForTest(int max) { + mMaxDynamicShortcuts = max; + } + + @VisibleForTesting + void setMaxDailyUpdatesForTest(int max) { + mMaxDailyUpdates = max; + } + + @VisibleForTesting + public void setResetIntervalForTest(long interval) { + mResetInterval = interval; + } +} diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index e51a2e1aa526..1603f1c12cd0 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -28,6 +28,8 @@ import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.view.WindowManager.DOCKED_TOP; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; +import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; import static android.view.WindowManager.LayoutParams.*; import static android.view.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static android.view.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; @@ -1239,7 +1241,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); - + mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); } } @@ -1269,12 +1271,20 @@ public class PhoneWindowManager implements WindowManagerPolicy { } }; - private final Runnable mScreenshotRunnable = new Runnable() { + private class ScreenshotRunnable implements Runnable { + private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN; + + public void setScreenshotType(int screenshotType) { + mScreenshotType = screenshotType; + } + @Override public void run() { - takeScreenshot(); + takeScreenshot(mScreenshotType); } - }; + } + + private final ScreenshotRunnable mScreenshotRunnable = new ScreenshotRunnable(); @Override public void showGlobalActions() { @@ -2304,29 +2314,33 @@ public class PhoneWindowManager implements WindowManagerPolicy { case TYPE_NAVIGATION_BAR_PANEL: // some panels (e.g. search) need to show on top of the navigation bar return 22; + case TYPE_SCREENSHOT: + // screenshot selection layer shouldn't go above system error, but it should cover + // navigation bars at the very least. + return 23; case TYPE_SYSTEM_ERROR: // system-level error dialogs - return 23; + return 24; case TYPE_MAGNIFICATION_OVERLAY: // used to highlight the magnified portion of a display - return 24; + return 25; case TYPE_DISPLAY_OVERLAY: // used to simulate secondary display devices - return 25; + return 26; case TYPE_DRAG: // the drag layer: input for drag-and-drop is associated with this window, // which sits above all other focusable windows - return 26; + return 27; case TYPE_ACCESSIBILITY_OVERLAY: // overlay put by accessibility services to intercept user interaction - return 27; - case TYPE_SECURE_SYSTEM_OVERLAY: return 28; - case TYPE_BOOT_PROGRESS: + case TYPE_SECURE_SYSTEM_OVERLAY: return 29; + case TYPE_BOOT_PROGRESS: + return 30; case TYPE_POINTER: // the (mouse) pointer layer - return 30; + return 31; } Log.e(TAG, "Unknown window type: " + type); return 2; @@ -3026,6 +3040,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } } + } else if (keyCode == KeyEvent.KEYCODE_S && event.isMetaPressed() + && event.isCtrlPressed()) { + if (down && repeatCount == 0) { + int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION + : TAKE_SCREENSHOT_FULLSCREEN; + mScreenshotRunnable.setScreenshotType(type); + mHandler.post(mScreenshotRunnable); + return -1; + } } else if (keyCode == KeyEvent.KEYCODE_SLASH && event.isMetaPressed()) { if (down) { if (repeatCount == 0) { @@ -3073,6 +3096,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } else if (keyCode == KeyEvent.KEYCODE_SYSRQ) { if (down && repeatCount == 0) { + mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mHandler.post(mScreenshotRunnable); } return -1; @@ -3360,8 +3384,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { throws RemoteException { synchronized (mLock) { IShortcutService service = mShortcutKeyServices.get(shortcutCode); - if (service != null && service.asBinder().isBinderAlive()) { - throw new RemoteException("Key already exists."); + if (service != null && service.asBinder().pingBinder()) { + throw new RemoteException("Key already exists."); } mShortcutKeyServices.put(shortcutCode, shortcutService); @@ -4402,9 +4426,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { "Laying out navigation bar window: (%d,%d - %d,%d)", pf.left, pf.top, pf.right, pf.bottom)); } else if ((attrs.type == TYPE_SECURE_SYSTEM_OVERLAY - || attrs.type == TYPE_BOOT_PROGRESS) + || attrs.type == TYPE_BOOT_PROGRESS + || attrs.type == TYPE_SCREENSHOT) && ((fl & FLAG_FULLSCREEN) != 0)) { - // Fullscreen secure system overlays get what they ask for. + // Fullscreen secure system overlays get what they ask for. Screenshot region + // selection overlay should also expand to full screen. pf.left = df.left = of.left = cf.left = mOverscanScreenLeft; pf.top = df.top = of.top = cf.top = mOverscanScreenTop; pf.right = df.right = of.right = cf.right = mOverscanScreenLeft @@ -5159,7 +5185,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { }; // Assume this is called from the Handler thread. - private void takeScreenshot() { + private void takeScreenshot(final int screenshotType) { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; @@ -5176,7 +5202,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return; } Messenger messenger = new Messenger(service); - Message msg = Message.obtain(null, 1); + Message msg = Message.obtain(null, screenshotType); final ServiceConnection myConn = this; Handler h = new Handler(mHandler.getLooper()) { @Override diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java index e5c5b2bc0f82..858f7c70663c 100644 --- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java +++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java @@ -81,6 +81,7 @@ public class TrustAgentWrapper { private boolean mBound; private long mScheduledRestartUptimeMillis; private long mMaximumTimeToLock; // from DevicePolicyManager + private boolean mPendingSuccessfulUnlock = false; // Trust state private boolean mTrusted; @@ -234,6 +235,11 @@ public class TrustAgentWrapper { setCallback(mCallback); updateDevicePolicyFeatures(); + if (mPendingSuccessfulUnlock) { + onUnlockAttempt(true); + mPendingSuccessfulUnlock = false; + } + if (mTrustManagerService.isDeviceLockedInner(mUserId)) { onDeviceLocked(); } else { @@ -302,7 +308,11 @@ public class TrustAgentWrapper { */ public void onUnlockAttempt(boolean successful) { try { - if (mTrustAgentService != null) mTrustAgentService.onUnlockAttempt(successful); + if (mTrustAgentService != null) { + mTrustAgentService.onUnlockAttempt(successful); + } else { + mPendingSuccessfulUnlock = successful; + } } catch (RemoteException e) { onError(e); } diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index b54e86660a47..984fb7627fea 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -19,7 +19,6 @@ package com.android.server.trust; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageMonitor; import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.LockPatternUtils.StrongAuthTracker; import com.android.server.SystemService; import org.xmlpull.v1.XmlPullParser; @@ -104,7 +103,7 @@ public class TrustManagerService extends SystemService { private static final int MSG_SET_DEVICE_LOCKED = 10; private static final int MSG_FLUSH_TRUST_USUALLY_MANAGED = 11; - public static final int TRUST_USUALLY_MANAGED_FLUSH_DELAY = 2 * 60 * 1000; + private static final int TRUST_USUALLY_MANAGED_FLUSH_DELAY = 2 * 60 * 1000; private final ArraySet<AgentInfo> mActiveAgents = new ArraySet<>(); private final ArrayList<ITrustListener> mTrustListeners = new ArrayList<>(); @@ -136,13 +135,7 @@ public class TrustManagerService extends SystemService { mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); mLockPatternUtils = new LockPatternUtils(context); - - mStrongAuthTracker = new StrongAuthTracker(context) { - @Override - public void onStrongAuthRequiredChanged(int userId) { - refreshAgentList(userId); - } - }; + mStrongAuthTracker = new StrongAuthTracker(context); } @Override @@ -231,24 +224,24 @@ public class TrustManagerService extends SystemService { TRUST_USUALLY_MANAGED_FLUSH_DELAY); } - void refreshAgentList(int userId) { - if (DEBUG) Slog.d(TAG, "refreshAgentList()"); + void refreshAgentList(int userIdOrAll) { + if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")"); if (!mTrustAgentsCanRun) { return; } - if (userId != UserHandle.USER_ALL && userId < UserHandle.USER_SYSTEM) { - Log.e(TAG, "refreshAgentList(userId=" + userId + "): Invalid user handle," + if (userIdOrAll != UserHandle.USER_ALL && userIdOrAll < UserHandle.USER_SYSTEM) { + Log.e(TAG, "refreshAgentList(userId=" + userIdOrAll + "): Invalid user handle," + " must be USER_ALL or a specific user.", new Throwable("here")); - userId = UserHandle.USER_ALL; + userIdOrAll = UserHandle.USER_ALL; } PackageManager pm = mContext.getPackageManager(); List<UserInfo> userInfos; - if (userId == UserHandle.USER_ALL) { + if (userIdOrAll == UserHandle.USER_ALL) { userInfos = mUserManager.getUsers(true /* excludeDying */); } else { userInfos = new ArrayList<>(); - userInfos.add(mUserManager.getUserInfo(userId)); + userInfos.add(mUserManager.getUserInfo(userIdOrAll)); } LockPatternUtils lockPatternUtils = mLockPatternUtils; @@ -261,7 +254,7 @@ public class TrustManagerService extends SystemService { if (!userInfo.supportsSwitchToByUser()) continue; if (!mActivityManager.isUserRunning(userInfo.id)) continue; if (!lockPatternUtils.isSecure(userInfo.id)) continue; - if (!mStrongAuthTracker.isTrustAllowedForUser(userInfo.id)) continue; + if (!mStrongAuthTracker.canAgentsRunForUser(userInfo.id)) continue; DevicePolicyManager dpm = lockPatternUtils.getDevicePolicyManager(); int disabledFeatures = dpm.getKeyguardDisabledFeatures(null, userInfo.id); final boolean disableTrustAgents = @@ -302,7 +295,7 @@ public class TrustManagerService extends SystemService { boolean trustMayHaveChanged = false; for (int i = 0; i < obsoleteAgents.size(); i++) { AgentInfo info = obsoleteAgents.valueAt(i); - if (userId == UserHandle.USER_ALL || userId == info.userId) { + if (userIdOrAll == UserHandle.USER_ALL || userIdOrAll == info.userId) { if (info.agent.isManagingTrust()) { trustMayHaveChanged = true; } @@ -312,10 +305,10 @@ public class TrustManagerService extends SystemService { } if (trustMayHaveChanged) { - if (userId == UserHandle.USER_ALL) { + if (userIdOrAll == UserHandle.USER_ALL) { updateTrustAll(); } else { - updateTrust(userId, 0); + updateTrust(userIdOrAll, 0); } } } @@ -578,6 +571,10 @@ public class TrustManagerService extends SystemService { } private void dispatchUnlockAttempt(boolean successful, int userId) { + if (successful) { + mStrongAuthTracker.allowTrustFromUnlock(userId); + } + for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { @@ -608,6 +605,10 @@ public class TrustManagerService extends SystemService { } private void dispatchOnTrustChanged(boolean enabled, int userId, int flags) { + if (DEBUG) { + Log.i(TAG, "onTrustChanged(" + enabled + ", " + userId + ", 0x" + + Integer.toHexString(flags) + ")"); + } if (!enabled) flags = 0; for (int i = 0; i < mTrustListeners.size(); i++) { try { @@ -623,6 +624,9 @@ public class TrustManagerService extends SystemService { } private void dispatchOnTrustManagedChanged(boolean managed, int userId) { + if (DEBUG) { + Log.i(TAG, "onTrustManagedChanged(" + managed + ", " + userId + ")"); + } for (int i = 0; i < mTrustListeners.size(); i++) { try { mTrustListeners.get(i).onTrustManagedChanged(managed, userId); @@ -980,4 +984,61 @@ public class TrustManagerService extends SystemService { null /* scheduler */); } } + + private class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker { + + SparseBooleanArray mStartFromSuccessfulUnlock = new SparseBooleanArray(); + + public StrongAuthTracker(Context context) { + super(context); + } + + @Override + public void onStrongAuthRequiredChanged(int userId) { + mStartFromSuccessfulUnlock.delete(userId); + + if (DEBUG) { + Log.i(TAG, "onStrongAuthRequiredChanged(" + userId + ") ->" + + " trustAllowed=" + isTrustAllowedForUser(userId) + + " agentsCanRun=" + canAgentsRunForUser(userId)); + } + + refreshAgentList(userId); + + // The list of active trust agents may not have changed, if there was a previous call + // to allowTrustFromUnlock, so we update the trust here too. + updateTrust(userId, 0 /* flags */); + } + + boolean canAgentsRunForUser(int userId) { + return mStartFromSuccessfulUnlock.get(userId) + || super.isTrustAllowedForUser(userId); + } + + /** + * Temporarily suppress strong auth requirements for {@param userId} until strong auth + * changes again. Must only be called when we know about a successful unlock already + * before the underlying StrongAuthTracker. + * + * Note that this only changes whether trust agents can be started, not the actual trusted + * value. + */ + void allowTrustFromUnlock(int userId) { + if (userId < UserHandle.USER_SYSTEM) { + throw new IllegalArgumentException("userId must be a valid user: " + userId); + } + boolean previous = canAgentsRunForUser(userId); + mStartFromSuccessfulUnlock.put(userId, true); + + if (DEBUG) { + Log.i(TAG, "allowTrustFromUnlock(" + userId + ") ->" + + " trustAllowed=" + isTrustAllowedForUser(userId) + + " agentsCanRun=" + canAgentsRunForUser(userId)); + } + + if (canAgentsRunForUser(userId) != previous) { + refreshAgentList(userId); + } + } + } } diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 49aaa4aa1047..30442bcba48f 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -1292,6 +1292,7 @@ public final class TvInputManagerService extends SystemService { @Override public void unblockContent( IBinder sessionToken, String unblockedRating, int userId) { + ensureParentalControlsPermission(); final int callingUid = Binder.getCallingUid(); final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, userId, "unblockContent"); diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index 2731f42991a5..9795c93e8e02 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -129,6 +129,7 @@ class AppWindowToken extends WindowToken { boolean mAlwaysFocusable; boolean mAppStopped; + int mPendingRelaunchCount; ArrayDeque<Rect> mFrozenBounds = new ArrayDeque<>(); @@ -531,6 +532,26 @@ class AppWindowToken extends WindowToken { } } + boolean isRelaunching() { + return mPendingRelaunchCount > 0; + } + + void startRelaunching() { + if (canFreezeBounds()) { + freezeBounds(); + } + mPendingRelaunchCount++; + } + + void finishRelaunching() { + if (canFreezeBounds()) { + unfreezeBounds(); + } + if (mPendingRelaunchCount > 0) { + mPendingRelaunchCount--; + } + } + void addWindow(WindowState w) { for (int i = allAppWindows.size() - 1; i >= 0; i--) { WindowState candidate = allAppWindows.get(i); @@ -579,20 +600,26 @@ class AppWindowToken extends WindowToken { } } + private boolean canFreezeBounds() { + // For freeform windows, we can't freeze the bounds at the moment because this would make + // the resizing unresponsive. + return mTask != null && !mTask.inFreeformWorkspace(); + } + /** * Freezes the task bounds. The size of this task reported the app will be fixed to the bounds * freezed by {@link Task#prepareFreezingBounds} until {@link #unfreezeBounds} gets called, even * if they change in the meantime. If the bounds are already frozen, the bounds will be frozen * with a queue. */ - void freezeBounds() { + private void freezeBounds() { mFrozenBounds.offer(new Rect(mTask.mPreparedFrozenBounds)); } /** * Unfreezes the previously frozen bounds. See {@link #freezeBounds}. */ - void unfreezeBounds() { + private void unfreezeBounds() { mFrozenBounds.remove(); for (int i = windows.size() - 1; i >= 0; i--) { final WindowState win = windows.get(i); @@ -658,7 +685,10 @@ class AppWindowToken extends WindowToken { pw.print(" startingMoved="); pw.println(startingMoved); } if (!mFrozenBounds.isEmpty()) { - pw.print(prefix); pw.print("mFrozenBounds="); pw.print(mFrozenBounds); + pw.print(prefix); pw.print("mFrozenBounds="); pw.println(mFrozenBounds); + } + if (mPendingRelaunchCount != 0) { + pw.print(prefix); pw.print("mPendingRelaunchCount="); pw.println(mPendingRelaunchCount); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b64aaa89475a..a998bc3be63b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -24,6 +24,9 @@ import android.app.ActivityManagerInternal; import android.app.ActivityManagerNative; import android.app.AppOpsManager; import android.app.IActivityManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -118,6 +121,7 @@ import android.view.animation.Animation; import android.view.inputmethod.InputMethodManagerInternal; import android.widget.Toast; +import com.android.internal.R; import com.android.internal.app.IAssistScreenshotReceiver; import com.android.internal.os.IResultReceiver; import com.android.internal.policy.IShortcutService; @@ -2340,6 +2344,7 @@ public class WindowManagerService extends IWindowManager.Stub mTokenMap.remove(token.token); } else if (atoken != null) { atoken.firstWindowDrawn = false; + atoken.allDrawn = false; } } @@ -4251,6 +4256,28 @@ public class WindowManagerService extends IWindowManager.Stub TAG_WM, "No longer Stopped: " + wtoken); wtoken.mAppStopped = false; wtoken.setWindowsExiting(false); + mOpeningApps.add(wtoken); + wtoken.startingMoved = false; + + // If the token is currently hidden (should be the + // common case), then we need to set up to wait for + // its windows to be ready. + if (wtoken.hidden) { + wtoken.allDrawn = false; + wtoken.deferClearAllDrawn = false; + wtoken.waitingToShow = true; + + if (wtoken.clientHidden) { + // In the case where we are making an app visible + // but holding off for a transition, we still need + // to tell the client to make its windows visible so + // they get drawn. Otherwise, we will wait on + // performing the transition until all windows have + // been drawn, they never will be, and we are sad. + wtoken.clientHidden = false; + wtoken.sendAppVisibilityToClients(); + } + } } // If we are preparing an app transition, then delay changing @@ -4268,29 +4295,7 @@ public class WindowManagerService extends IWindowManager.Stub } wtoken.inPendingTransaction = true; if (visible) { - mOpeningApps.add(wtoken); - wtoken.startingMoved = false; wtoken.mEnteringAnimation = true; - - // If the token is currently hidden (should be the - // common case), then we need to set up to wait for - // its windows to be ready. - if (wtoken.hidden) { - wtoken.allDrawn = false; - wtoken.deferClearAllDrawn = false; - wtoken.waitingToShow = true; - - if (wtoken.clientHidden) { - // In the case where we are making an app visible - // but holding off for a transition, we still need - // to tell the client to make its windows visible so - // they get drawn. Otherwise, we will wait on - // performing the transition until all windows have - // been drawn, they never will be, and we are sad. - wtoken.clientHidden = false; - wtoken.sendAppVisibilityToClients(); - } - } } else { wtoken.setWindowsExiting(true); mClosingApps.add(wtoken); @@ -7468,6 +7473,35 @@ public class WindowManagerService extends IWindowManager.Stub return mCurrentFocus; } + private void showAuditSafeModeNotification() { + PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, + new Intent(Intent.ACTION_VIEW, + Uri.parse("https://support.google.com/nexus/answer/2852139")), 0); + + String title = mContext.getString(R.string.audit_safemode_notification); + + Notification notification = new Notification.Builder(mContext) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning) + .setWhen(0) + .setOngoing(true) + .setTicker(title) + .setLocalOnly(true) + .setPriority(Notification.PRIORITY_HIGH) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setColor(mContext.getColor( + com.android.internal.R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentText(mContext.getString(R.string.audit_safemode_notification_details)) + .setContentIntent(pendingIntent) + .build(); + + NotificationManager notificationManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.notifyAsUser(null, R.string.audit_safemode_notification, notification, + UserHandle.ALL); + } + public boolean detectSafeMode() { if (!mInputMonitor.waitForInputDevicesReady( INPUT_DEVICES_READY_FOR_SAFE_MODE_DETECTION_TIMEOUT_MILLIS)) { @@ -7500,6 +7534,7 @@ public class WindowManagerService extends IWindowManager.Stub if (auditSafeMode >= buildDate) { mSafeMode = true; + showAuditSafeModeNotification(); } else { SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, ""); SystemProperties.set(ShutdownThread.AUDIT_SAFEMODE_PROPERTY, ""); @@ -9636,8 +9671,8 @@ public class WindowManagerService extends IWindowManager.Stub public void notifyAppRelaunching(IBinder token) { synchronized (mWindowMap) { AppWindowToken appWindow = findAppWindowToken(token); - if (canFreezeBounds(appWindow)) { - appWindow.freezeBounds(); + if (appWindow != null) { + appWindow.startRelaunching(); } } } @@ -9645,20 +9680,12 @@ public class WindowManagerService extends IWindowManager.Stub public void notifyAppRelaunchingFinished(IBinder token) { synchronized (mWindowMap) { AppWindowToken appWindow = findAppWindowToken(token); - if (canFreezeBounds(appWindow)) { - appWindow.unfreezeBounds(); + if (appWindow != null) { + appWindow.finishRelaunching(); } } } - private boolean canFreezeBounds(AppWindowToken appWindow) { - - // For freeform windows, we can't freeze the bounds at the moment because this would make - // the resizing unresponsive. - return appWindow != null && appWindow.mTask != null - && !appWindow.mTask.inFreeformWorkspace(); - } - @Override public int getDockedDividerInsetsLw() { return getDefaultDisplayContentLocked().getDockedDividerController().getContentInsets(); diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java index 18aa18621d63..8ada2f1a1646 100644 --- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java +++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java @@ -1280,7 +1280,12 @@ class WindowSurfacePlacer { "Check opening app=" + wtoken + ": allDrawn=" + wtoken.allDrawn + " startingDisplayed=" + wtoken.startingDisplayed + " startingMoved=" - + wtoken.startingMoved); + + wtoken.startingMoved + " isRelaunching()=" + + wtoken.isRelaunching()); + + if (wtoken.isRelaunching()) { + return false; + } final boolean drawnBeforeRestoring = wtoken.allDrawn; wtoken.restoreSavedSurfaces(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index d97967515caf..4831db5f3dc3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -33,6 +33,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accounts.AccountManager; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManagerNative; @@ -3642,7 +3643,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (count == 0 || count > admin.maximumFailedPasswordsForWipe || (count == admin.maximumFailedPasswordsForWipe && - mUserManager.getUserInfo(userId).isPrimary())) { + getUserInfo(userId).isPrimary())) { count = admin.maximumFailedPasswordsForWipe; strictestAdmin = admin; } @@ -3650,6 +3651,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return strictestAdmin; } + private UserInfo getUserInfo(@UserIdInt int userId) { + final long token = mInjector.binderClearCallingIdentity(); + try { + return mUserManager.getUserInfo(userId); + } finally { + mInjector.binderRestoreCallingIdentity(token); + } + } + @Override public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException { if (!mHasFeature) { @@ -5913,7 +5923,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * - adb if there are not accounts. */ private void enforceCanSetProfileOwnerLocked(int userHandle) { - UserInfo info = mUserManager.getUserInfo(userHandle); + UserInfo info = getUserInfo(userHandle); if (info == null) { // User doesn't exist. throw new IllegalArgumentException( @@ -6065,12 +6075,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private boolean isManagedProfile(int userHandle) { - long ident = mInjector.binderClearCallingIdentity(); - try { - return mUserManager.getUserInfo(userHandle).isManagedProfile(); - } finally { - mInjector.binderRestoreCallingIdentity(ident); - } + return getUserInfo(userHandle).isManagedProfile(); } private void enableIfNecessary(String packageName, int userId) { @@ -6409,7 +6414,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { try { // If we have an enabled packages list for a managed profile the packages // we should check are installed for the parent user. - UserInfo user = mUserManager.getUserInfo(userIdToCheck); + UserInfo user = getUserInfo(userIdToCheck); if (user.isManagedProfile()) { userIdToCheck = user.profileGroupId; } @@ -6455,7 +6460,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { List<AccessibilityServiceInfo> enabledServices = null; long id = mInjector.binderClearCallingIdentity(); try { - UserInfo user = mUserManager.getUserInfo(userId); + UserInfo user = getUserInfo(userId); if (user.isManagedProfile()) { userId = user.profileGroupId; } @@ -6537,7 +6542,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (result != null) { long id = mInjector.binderClearCallingIdentity(); try { - UserInfo user = mUserManager.getUserInfo(userId); + UserInfo user = getUserInfo(userId); if (user.isManagedProfile()) { userId = user.profileGroupId; } @@ -6591,7 +6596,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { long token = mInjector.binderClearCallingIdentity(); try { UserInfo currentUser; - UserInfo callingUser = mUserManager.getUserInfo(callingUserId); + UserInfo callingUser = getUserInfo(callingUserId); try { currentUser = mInjector.getIActivityManager().getCurrentUser(); } catch (RemoteException e) { @@ -8188,10 +8193,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public String getWifiMacAddress() { + public String getWifiMacAddress(ComponentName admin) { // Make sure caller has DO. synchronized (this) { - getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER); + getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER); } final long ident = mInjector.binderClearCallingIdentity(); @@ -8223,13 +8228,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); } final int callingUserId = mInjector.userHandleGetCallingUserId(); - final UserInfo user; - long ident = mInjector.binderClearCallingIdentity(); - try { - user = mUserManager.getUserInfo(callingUserId); - } finally { - mInjector.binderRestoreCallingIdentity(ident); - } + final UserInfo user = getUserInfo(callingUserId); return user != null && user.isManagedProfile(); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 68fd0f681aa5..b316cbd96453 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -494,7 +494,6 @@ class Owners { abstract void writeInner(XmlSerializer out) throws IOException; abstract boolean readInner(XmlPullParser parser, int depth, String tag); - } private class DeviceOwnerReadWriter extends FileReadWriter { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index a2d5259b2758..59f8284a5200 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -83,6 +83,7 @@ import com.android.server.pm.Installer; import com.android.server.pm.LauncherAppsService; import com.android.server.pm.OtaDexoptService; import com.android.server.pm.PackageManagerService; +import com.android.server.pm.ShortcutService; import com.android.server.pm.UserManagerService; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; @@ -725,6 +726,9 @@ public final class SystemServer { // Always start the Device Policy Manager, so that the API is compatible with // API8. mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class); + +// TODO is this a good place? + mSystemServiceManager.startService(ShortcutService.Lifecycle.class); } if (!disableSystemUI) { diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 3ae10729f741..23f186c42ce3 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -106,6 +106,10 @@ <service android:name="com.android.server.job.MockPriorityJobService" android:permission="android.permission.BIND_JOB_SERVICE" /> + + <activity android:name="com.android.server.pm.ShortcutManagerTest$ShortcutActivity" /> + <activity android:name="com.android.server.pm.ShortcutManagerTest$ShortcutActivity2" /> + <activity android:name="com.android.server.pm.ShortcutManagerTest$ShortcutActivity3" /> </application> <instrumentation diff --git a/services/tests/servicestests/res/drawable/icon1.png b/services/tests/servicestests/res/drawable/icon1.png Binary files differnew file mode 100644 index 000000000000..64eb294363d2 --- /dev/null +++ b/services/tests/servicestests/res/drawable/icon1.png diff --git a/services/tests/servicestests/res/drawable/icon2.png b/services/tests/servicestests/res/drawable/icon2.png Binary files differnew file mode 100644 index 000000000000..75024841d327 --- /dev/null +++ b/services/tests/servicestests/res/drawable/icon2.png diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 8c47087f4305..e897e3d20edf 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1444,10 +1444,10 @@ public class DevicePolicyManagerTest extends DpmTestBase { // Test 1. Caller doesn't have DO or DA. try { - dpm.getWifiMacAddress(); + dpm.getWifiMacAddress(admin1); fail(); } catch (SecurityException e) { - MoreAsserts.assertContainsRegex("No active admin owned", e.getMessage()); + MoreAsserts.assertContainsRegex("No active admin", e.getMessage()); } // DO needs to be an DA. @@ -1456,19 +1456,19 @@ public class DevicePolicyManagerTest extends DpmTestBase { // Test 2. Caller has DA, but not DO. try { - dpm.getWifiMacAddress(); + dpm.getWifiMacAddress(admin1); fail(); } catch (SecurityException e) { - MoreAsserts.assertContainsRegex("No active admin owned", e.getMessage()); + MoreAsserts.assertContainsRegex("does not own the device", e.getMessage()); } // Test 3. Caller has PO, but not DO. assertTrue(dpm.setProfileOwner(admin1, null, UserHandle.USER_SYSTEM)); try { - dpm.getWifiMacAddress(); + dpm.getWifiMacAddress(admin1); fail(); } catch (SecurityException e) { - MoreAsserts.assertContainsRegex("No active admin owned", e.getMessage()); + MoreAsserts.assertContainsRegex("does not own the device", e.getMessage()); } // Remove PO. @@ -1478,17 +1478,17 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertTrue(dpm.setDeviceOwner(admin1, null, UserHandle.USER_SYSTEM)); // 4-1. But no WifiInfo. - assertNull(dpm.getWifiMacAddress()); + assertNull(dpm.getWifiMacAddress(admin1)); // 4-2. Returns WifiInfo, but with the default MAC. when(mContext.wifiManager.getConnectionInfo()).thenReturn(new WifiInfo()); - assertNull(dpm.getWifiMacAddress()); + assertNull(dpm.getWifiMacAddress(admin1)); // 4-3. With a real MAC address. final WifiInfo wi = new WifiInfo(); wi.setMacAddress("11:22:33:44:55:66"); when(mContext.wifiManager.getConnectionInfo()).thenReturn(wi); - assertEquals("11:22:33:44:55:66", dpm.getWifiMacAddress()); + assertEquals("11:22:33:44:55:66", dpm.getWifiMacAddress(admin1)); } public void testRebootCanOnlyBeCalledByDeviceOwner() throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutInfoTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutInfoTest.java new file mode 100644 index 000000000000..eb16a1db5876 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutInfoTest.java @@ -0,0 +1,44 @@ + +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.content.pm.ShortcutInfo; +import android.test.AndroidTestCase; + +import com.android.server.testutis.TestUtils; + +/** + * Tests for {@link ShortcutInfo}. + + m FrameworksServicesTests && + adb install \ + -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk && + adb shell am instrument -e class com.android.server.pm.ShortcutInfoTest \ + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + + */ +public class ShortcutInfoTest extends AndroidTestCase { + + public void testNoId() { + TestUtils.assertExpectException( + IllegalArgumentException.class, + "ID must be provided", + () -> new ShortcutInfo.Builder(mContext).build()); + } + + // TODO Add more tests. +} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java new file mode 100644 index 000000000000..1f805e9116ba --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java @@ -0,0 +1,1206 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps.ShortcutQuery; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.pm.ShortcutServiceInternal; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.FileUtils; +import android.os.UserHandle; +import android.test.AndroidTestCase; +import android.test.mock.MockContext; +import android.util.Log; + +import com.android.frameworks.servicestests.R; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; +import com.android.server.SystemService; + +import org.junit.Assert; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * Tests for ShortcutService and ShortcutManager. + * + m FrameworksServicesTests && + adb install \ + -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk && + adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest \ + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + */ +public class ShortcutManagerTest extends AndroidTestCase { + private static final String TAG = "ShortcutManagerTest"; + + /** + * Whether to enable dump or not. Should be only true when debugging to avoid bugs where + * dump affecting the behavior. + */ + private static final boolean ENABLE_DUMP = true; // DO NOT SUBMIT WITH true + + /** Context used in the client side */ + private final class ClientContext extends MockContext { + @Override + public String getPackageName() { + return mInjectedClientPackage; + } + } + + /** Context used in the service side */ + private final class ServiceContext extends MockContext { + } + + /** ShortcutService with injection override methods. */ + private final class ShortcutServiceTestable extends ShortcutService { + public ShortcutServiceTestable(Context context) { + super(context); + + } + + @Override + void injectLoadConfigurationLocked() { + setResetIntervalForTest(INTERVAL); + setMaxDynamicShortcutsForTest(MAX_SHORTCUTS); + setMaxDailyUpdatesForTest(MAX_DAILY_UPDATES); + } + + @Override + long injectCurrentTimeMillis() { + return mInjectedCurrentTimeLillis; + } + + @Override + int injectBinderCallingUid() { + return mInjectedCallingUid; + } + + @Override + int injectGetPackageUid(String packageName) { + Integer uid = mInjectedPackageUidMap.get(packageName); + return uid != null ? uid : -1; + } + + @Override + File injectSystemDataPath() { + return new File(mInjectedFilePathRoot, "system"); + } + + @Override + File injectUserDataPath(@UserIdInt int userId) { + return new File(mInjectedFilePathRoot, "user-" + userId); + } + } + + /** ShortcutManager with injection override methods. */ + private final class ShortcutManagerTestable extends ShortcutManager { + public ShortcutManagerTestable(Context context, ShortcutServiceTestable service) { + super(context, service); + } + + @Override + protected int injectMyUserId() { + return UserHandle.getUserId(mInjectedCallingUid); + } + } + + + public static class ShortcutActivity extends Activity { + } + + public static class ShortcutActivity2 extends Activity { + } + + public static class ShortcutActivity3 extends Activity { + } + + private ServiceContext mServiceContext; + private ClientContext mClientContext; + + private ShortcutServiceTestable mService; + private ShortcutManagerTestable mManager; + private ShortcutServiceInternal mInternal; + + private File mInjectedFilePathRoot; + + private long mInjectedCurrentTimeLillis; + + private int mInjectedCallingUid; + private String mInjectedClientPackage; + + private Map<String, Integer> mInjectedPackageUidMap; + + private static final String CALLING_PACKAGE_1 = "com.android.test.1"; + private static final int CALLING_UID_1 = 10001; + + private static final String CALLING_PACKAGE_2 = "com.android.test.2"; + private static final int CALLING_UID_2 = 10002; + + private static final String CALLING_PACKAGE_3 = "com.android.test.3"; + private static final int CALLING_UID_3 = 10003; + + private static final String LAUNCHER_1 = "com.android.launcher.1"; + private static final int LAUNCHER_UID_1 = 10011; + + private static final String LAUNCHER_2 = "com.android.launcher.2"; + private static final int LAUNCHER_UID_2 = 10012; + + private static final long START_TIME = 1234560000000L; + + private static final long INTERVAL = 10000; + + private static final int MAX_SHORTCUTS = 5; + + private static final int MAX_DAILY_UPDATES = 3; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mServiceContext = new ServiceContext(); + mClientContext = new ClientContext(); + + // Prepare injection values. + + mInjectedCurrentTimeLillis = START_TIME; + + mInjectedPackageUidMap = new HashMap<>(); + mInjectedPackageUidMap.put(CALLING_PACKAGE_1, CALLING_UID_1); + mInjectedPackageUidMap.put(CALLING_PACKAGE_2, CALLING_UID_2); + mInjectedPackageUidMap.put(CALLING_PACKAGE_3, CALLING_UID_3); + mInjectedPackageUidMap.put(LAUNCHER_1, LAUNCHER_UID_1); + mInjectedPackageUidMap.put(LAUNCHER_2, LAUNCHER_UID_2); + + mInjectedFilePathRoot = new File(getContext().getCacheDir(), "test-files"); + + // Empty the data directory. + if (mInjectedFilePathRoot.exists()) { + Assert.assertTrue("failed to delete dir", + FileUtils.deleteContents(mInjectedFilePathRoot)); + } + mInjectedFilePathRoot.mkdirs(); + + initService(); + setCaller(CALLING_PACKAGE_1); + } + + /** (Re-) init the manager and the service. */ + private void initService() { + LocalServices.removeServiceForTest(ShortcutServiceInternal.class); + + // Instantiate targets. + mService = new ShortcutServiceTestable(mServiceContext); + mManager = new ShortcutManagerTestable(mClientContext, mService); + + mInternal = LocalServices.getService(ShortcutServiceInternal.class); + + // Load the setting file. + mService.onBootPhase(SystemService.PHASE_LOCK_SETTINGS_READY); + } + + /** Replace the current calling package */ + private void setCaller(String packageName) { + mInjectedClientPackage = packageName; + mInjectedCallingUid = Preconditions.checkNotNull(mInjectedPackageUidMap.get(packageName)); + } + + private String getCallingPackage() { + return mInjectedClientPackage; + } + + private int getCallingUserId() { + return UserHandle.getUserId(mInjectedCallingUid); + } + + /** For debugging */ + private void dumpsysOnLogcat() { + if (!ENABLE_DUMP) return; + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final PrintWriter pw = new PrintWriter(out); + mService.dumpInner(pw); + pw.close(); + + Log.e(TAG, "Dumping ShortcutService:"); + for (String line : out.toString().split("\n")) { + Log.e(TAG, line); + } + } + + /** + * For debugging, dump arbitrary file on logcat. + */ + private void dumpFileOnLogcat(String path) { + if (!ENABLE_DUMP) return; + + Log.i(TAG, "Dumping file: " + path); + final StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + while ((line = br.readLine()) != null) { + Log.i(TAG, line); + } + } catch (Exception e) { + Log.e(TAG, "Couldn't read file", e); + fail("Exception " + e); + } + } + + /** + * For debugging, dump the main state file on logcat. + */ + private void dumpBaseStateFile() { + dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath() + + "/system/" + ShortcutService.FILENAME_BASE_STATE); + } + + /** + * For debugging, dump per-user state file on logcat. + */ + private void dumpUserFile(int userId) { + dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath() + + "/user-" + userId + + "/" + ShortcutService.FILENAME_USER_PACKAGES); + } + + private static Bundle makeBundle(Object... keysAndValues) { + Preconditions.checkState((keysAndValues.length % 2) == 0); + + if (keysAndValues.length == 0) { + return null; + } + final Bundle ret = new Bundle(); + + for (int i = keysAndValues.length - 2; i >= 0; i -= 2) { + final String key = keysAndValues[i].toString(); + final Object value = keysAndValues[i + 1]; + + if (value == null) { + ret.putString(key, null); + } else if (value instanceof Integer) { + ret.putInt(key, (Integer) value); + } else if (value instanceof String) { + ret.putString(key, (String) value); + } else if (value instanceof Bundle) { + ret.putBundle(key, (Bundle) value); + } else { + fail("Type not supported yet: " + value.getClass().getName()); + } + } + return ret; + } + + /** + * Make a shortcut with an ID. + */ + private ShortcutInfo makeShortcut(String id) { + return makeShortcut( + id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + } + + /** + * Make a shortcut with an ID and timestamp. + */ + private ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) { + final ShortcutInfo s = makeShortcut( + id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + s.setTimestamp(timestamp); + return s; + } + + /** + * Make multiple shortcuts with IDs. + */ + private List<ShortcutInfo> makeShortcuts(String... ids) { + final ArrayList<ShortcutInfo> ret = new ArrayList(); + for (String id : ids) { + ret.add(makeShortcut(id)); + } + return ret; + } + + /** + * Make a shortcut with details. + */ + private ShortcutInfo makeShortcut(String id, String title, ComponentName activity, + Icon icon, Intent intent, int weight) { + final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext) + .setId(id) + .setTitle(title) + .setWeight(weight) + .setIntent(intent); + if (icon != null) { + b.setIcon(icon); + } + if (activity != null) { + b.setActivityComponent(activity); + } + final ShortcutInfo s = b.build(); + + s.setTimestamp(mInjectedCurrentTimeLillis); // HACK + + return s; + } + + /** + * Make an intent. + */ + private Intent makeIntent(String action, Class<?> clazz, Object... bundleKeysAndValues) { + final Intent intent = new Intent(action); + intent.setComponent(makeComponent(clazz)); + intent.replaceExtras(makeBundle(bundleKeysAndValues)); + return intent; + } + + /** + * Make an component name, with the client context. + */ + @NonNull + private ComponentName makeComponent(Class<?> clazz) { + return new ComponentName(mClientContext, clazz); + } + + @NonNull + private ShortcutInfo findById(List<ShortcutInfo> list, String id) { + for (ShortcutInfo s : list) { + if (s.getId().equals(id)) { + return s; + } + } + fail("Shortcut with id " + id + " not found"); + return null; + } + + private void assertResetTimes(long expectedLastResetTime, long expectedNextResetTime) { + assertEquals(expectedLastResetTime, mService.getLastResetTimeLocked()); + assertEquals(expectedNextResetTime, mService.getNextResetTimeLocked()); + } + + @NonNull + private List<ShortcutInfo> assertShortcutIds(@NonNull List<ShortcutInfo> actualShortcuts, + String... expectedIds) { + final HashSet<String> expected = new HashSet<>(Arrays.asList(expectedIds)); + final HashSet<String> actual = new HashSet<>(); + for (ShortcutInfo s : actualShortcuts) { + actual.add(s.getId()); + } + + // Compare the sets. + assertEquals(expected, actual); + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllHaveIntents( + @NonNull List<ShortcutInfo> actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNotNull("ID " + s.getId(), s.getIntent()); + } + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllNotHaveIntents( + @NonNull List<ShortcutInfo> actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNull("ID " + s.getId(), s.getIntent()); + } + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllHaveTitle( + @NonNull List<ShortcutInfo> actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNotNull("ID " + s.getId(), s.getTitle()); + } + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllNotHaveTitle( + @NonNull List<ShortcutInfo> actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNull("ID " + s.getId(), s.getTitle()); + } + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllHaveFlags(@NonNull List<ShortcutInfo> actualShortcuts, + int shortcutFlags) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.hasFlags(shortcutFlags)); + } + return actualShortcuts; + } + + @NonNull + private List<ShortcutInfo> assertAllDynamic(@NonNull List<ShortcutInfo> actualShortcuts) { + return assertAllHaveFlags(actualShortcuts, ShortcutInfo.FLAG_DYNAMIC); + } + + @NonNull + private List<ShortcutInfo> assertAllPinned(@NonNull List<ShortcutInfo> actualShortcuts) { + return assertAllHaveFlags(actualShortcuts, ShortcutInfo.FLAG_PINNED); + } + + @NonNull + private List<ShortcutInfo> assertAllDynamicOrPinned( + @NonNull List<ShortcutInfo> actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isDynamic() || s.isPinned()); + } + return actualShortcuts; + } + + /** + * Test for the first launch path, no settings file available. + */ + public void testFirstInitialize() { + assertResetTimes(START_TIME, START_TIME + INTERVAL); + } + + /** + * Test for {@link ShortcutService#updateTimes()} + */ + public void testUpdateAndGetNextResetTimeLocked() { + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock. + mInjectedCurrentTimeLillis += 100; + + // Shouldn't have changed. + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock, almost the reset time. + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + + // Shouldn't have changed. + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock. + mInjectedCurrentTimeLillis += 1; + + assertResetTimes(START_TIME + INTERVAL, START_TIME + 2 * INTERVAL); + + // Advance further; 4 days since start. + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + } + + /** + * Test for the restoration from saved file. + */ + public void testInitializeFromSavedFile() { + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + + mService.saveBaseStateLocked(); + + dumpBaseStateFile(); + + // Restore. + initService(); + + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + } + + /** + * Test for the restoration from restored file. + */ + public void testLoadFromBrokenFile() { + // TODO Add various broken cases. + } + + // === Test for app side APIs === + + /** Test for {@link android.content.pm.ShortcutManager#getMaxDynamicShortcutCount()} */ + public void testGetMaxDynamicShortcutCount() { + assertEquals(MAX_SHORTCUTS, mManager.getMaxDynamicShortcutCount()); + } + + /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */ + public void testGetRemainingCallCount() { + assertEquals(MAX_DAILY_UPDATES, mManager.getRemainingCallCount()); + } + + /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */ + public void testGetRateLimitResetTime() { + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + + assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime()); + } + + public void testSetDynamicShortcuts() { + final Icon icon1 = Icon.createWithResource(mContext, R.drawable.icon1); + final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), R.drawable.icon2)); + + final ShortcutInfo si1 = makeShortcut( + "shortcut1", + "Title 1", + makeComponent(ShortcutActivity.class), + icon1, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo si2 = makeShortcut( + "shortcut2", + "Title 2", + /* activity */ null, + icon2, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2))); + assertEquals(2, mManager.getDynamicShortcuts().size()); + assertEquals(2, mManager.getRemainingCallCount()); + + // TODO: Check fields + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getDynamicShortcuts().size()); + assertEquals(1, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList())); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getRemainingCallCount()); + + dumpsysOnLogcat(); + + mInjectedCurrentTimeLillis++; // Need to advance the clock for reset to work. + mService.resetThrottlingInner(); + + dumpsysOnLogcat(); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2, si3))); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + // TODO Check max number + } + + public void testAddDynamicShortcuts() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + assertEquals(1, mManager.getDynamicShortcuts().size()); + + assertTrue(mManager.addDynamicShortcut(si2)); + assertEquals(1, mManager.getRemainingCallCount()); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + // Add with the same ID + assertTrue(mManager.addDynamicShortcut(makeShortcut("shortcut1"))); + assertEquals(0, mManager.getRemainingCallCount()); + assertEquals(2, mManager.getDynamicShortcuts().size()); // Still 2 + + // TODO Check max number + + // TODO Check fields. + } + + public void testDeleteDynamicShortcut() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + assertEquals(2, mManager.getRemainingCallCount()); + + mManager.deleteDynamicShortcut("shortcut1"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut1"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcutXXX"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut2"); + assertEquals(1, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut3"); + assertEquals(0, mManager.getDynamicShortcuts().size()); + + // Still 2 calls left. + assertEquals(2, mManager.getRemainingCallCount()); + + // TODO Make sure pinned shortcuts won't be deleted. + } + + public void testDeleteAllDynamicShortcuts() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + assertEquals(2, mManager.getRemainingCallCount()); + + mManager.deleteAllDynamicShortcuts(); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(2, mManager.getRemainingCallCount()); + + // Note delete shouldn't affect throttling, so... + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getDynamicShortcuts().size()); + + // This should still work. + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + // Still 1 call left + assertEquals(1, mManager.getRemainingCallCount()); + + // TODO Make sure pinned shortcuts won't be deleted. + } + + public void testThrottling() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Reached the max + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Still throttled + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Now it should work. + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // 4 days later... + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + // Make sure getRemainingCallCount() itself gets reset withou calling setDynamicShortcuts(). + mInjectedCurrentTimeLillis = START_TIME + 8 * INTERVAL; + assertEquals(3, mManager.getRemainingCallCount()); + } + + public void testThrottling_perPackage() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Reached the max + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + // Try from a different caller. + mInjectedClientPackage = CALLING_PACKAGE_2; + mInjectedCallingUid = CALLING_UID_2; + + // Need to create a new one wit the updated package name. + final ShortcutInfo si2 = makeShortcut("shortcut1"); + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertEquals(1, mManager.getRemainingCallCount()); + + // Back to the original caller, still throttled. + mInjectedClientPackage = CALLING_PACKAGE_1; + mInjectedCallingUid = CALLING_UID_1; + + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + assertEquals(0, mManager.getRemainingCallCount()); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Now it should work. + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedClientPackage = CALLING_PACKAGE_2; + mInjectedCallingUid = CALLING_UID_2; + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si2))); + } + + // TODO: updateShortcuts() + // TODO: getPinnedShortcuts() + + // === Test for launcher side APIs === + + public void testGetShortcuts() { + + // Set up shortcuts. + + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 5000); + final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 1000); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); + final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000); + final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2, s2_3, s2_4))); + + setCaller(CALLING_PACKAGE_3); + final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s3", 5000); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s3_2))); + + setCaller(LAUNCHER_1); + + // Get dynamic + assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC, getCallingUserId()), + "s1", "s2")))); + + // Get pinned + assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED, getCallingUserId()) + /* none */); + + // Get both, with timestamp + assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC, + getCallingUserId()), + "s2", "s3")))); + + // FLAG_GET_KEY_FIELDS_ONLY + assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY, + getCallingUserId()), + "s2", "s3")))); + + // Pin some shortcuts. + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s3", "s4"), getCallingUserId()); + + // Pinned ones only + assertAllPinned(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED, + getCallingUserId()), + "s3")))); + + // All packages. + assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), + /* time =*/ 5000, /* package= */ null, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED, + getCallingUserId()), + "s1", "s3"); + + // TODO More tests: pinned but dynamic, filter by activity + } + + public void testGetShortcutInfo() { + // Create shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcut( + "s1", + "Title 1", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo s1_2 = makeShortcut( + "s2", + "Title 2", + /* activity */ null, + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + dumpsysOnLogcat(); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_1 = makeShortcut( + "s1", + "ABC", + makeComponent(ShortcutActivity2.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ANSWER, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_1))); + dumpsysOnLogcat(); + + // Pin some. + setCaller(LAUNCHER_1); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2"), getCallingUserId()); + + dumpsysOnLogcat(); + + // Delete some. + setCaller(CALLING_PACKAGE_1); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + dumpsysOnLogcat(); + + setCaller(LAUNCHER_1); + List<ShortcutInfo> list; + + // Note we don't guarantee the orders. + list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2", "s1", "s3", null), getCallingUserId()))), + "s1", "s2"); + assertEquals("Title 1", findById(list, "s1").getTitle()); + assertEquals("Title 2", findById(list, "s2").getTitle()); + + assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s3"), getCallingUserId()))) + /* none */); + + list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s1", "s2", "s3"), getCallingUserId()))), + "s1"); + assertEquals("ABC", findById(list, "s1").getTitle()); + } + + public void testPinShortcutAndGetPinnedShortcuts() { + // Create some shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); + final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); + final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000); + final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2, s2_3, s2_4))); + + setCaller(CALLING_PACKAGE_3); + final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s2", 1000); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2))); + + // Pin some. + setCaller(LAUNCHER_1); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2", "s3"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s3", "s4", "s5"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_3, + Arrays.asList("s3"), getCallingUserId()); // Note ID doesn't exist + + // Delete some. + setCaller(CALLING_PACKAGE_1); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + setCaller(CALLING_PACKAGE_2); + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + mManager.deleteDynamicShortcut("s3"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + + setCaller(CALLING_PACKAGE_3); + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + + // Get pinned shortcuts from launcher + setCaller(LAUNCHER_1); + + // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists. + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())), + "s2"); + + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_2, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())), + "s3", "s4"); + + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_3, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())) + /* none */); + } + + public void testCreateShortcutIntent() { + // Create some shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcut( + "s1", + "Title 1", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo s1_2 = makeShortcut( + "s2", + "Title 2", + /* activity */ null, + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_1 = makeShortcut( + "s1", + "ABC", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ANSWER, ShortcutActivity.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_1))); + + // Pin all. + setCaller(LAUNCHER_1); + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s1", "s2"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s1"), getCallingUserId()); + + // Just to make it complicated, delete some. + setCaller(CALLING_PACKAGE_1); + mManager.deleteDynamicShortcut("s2"); + + // intent and check. + setCaller(LAUNCHER_1); + Intent intent; + intent = mInternal.createShortcutIntent(getCallingPackage(), + CALLING_PACKAGE_1, "s1", getCallingUserId()); + assertEquals(ShortcutActivity2.class.getName(), intent.getComponent().getClassName()); + + intent = mInternal.createShortcutIntent(getCallingPackage(), + CALLING_PACKAGE_1, "s2", getCallingUserId()); + assertEquals(ShortcutActivity3.class.getName(), intent.getComponent().getClassName()); + + intent = mInternal.createShortcutIntent(getCallingPackage(), + CALLING_PACKAGE_2, "s1", getCallingUserId()); + assertEquals(ShortcutActivity.class.getName(), intent.getComponent().getClassName()); + + // TODO Check extra, etc + } + + // === Test for persisting === + + public void testSaveAndLoadUser_empty() { + assertTrue(mManager.setDynamicShortcuts(Arrays.asList())); + + Log.i(TAG, "Saved state"); + dumpsysOnLogcat(); + dumpUserFile(0); + + // Restore. + initService(); + + assertEquals(0, mManager.getDynamicShortcuts().size()); + } + + /** + * Try save and load, also stop/start the user. + */ + public void testSaveAndLoadUser() { + // First, create some shortcuts and save. + final Icon icon1 = Icon.createWithResource(mContext, R.drawable.icon1); + final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), R.drawable.icon2)); + + final ShortcutInfo si1 = makeShortcut( + "shortcut1", + "Title 1", + makeComponent(ShortcutActivity.class), + icon1, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo si2 = makeShortcut( + "shortcut2", + "Title 2", + /* activity */ null, + icon2, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2))); + + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + assertEquals(2, mManager.getRemainingCallCount()); + + Log.i(TAG, "Saved state"); + dumpsysOnLogcat(); + dumpUserFile(0); + + // Restore. + initService(); + + // Before the load, the map should be empty. + assertEquals(0, mService.getShortcutsForTest().size()); + + // this will pre-load the per-user info. + mService.onStartUserLocked(UserHandle.USER_SYSTEM); + + // Now it's loaded. + assertEquals(1, mService.getShortcutsForTest().size()); + + // Start another user + mService.onStartUserLocked(10); + + // Now the size is 2. + assertEquals(2, mService.getShortcutsForTest().size()); + + Log.i(TAG, "Dumping the new instance"); + + List<ShortcutInfo> loaded = mManager.getDynamicShortcuts(); + + Log.i(TAG, "Loaded state"); + dumpsysOnLogcat(); + + assertEquals(2, loaded.size()); + + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + assertEquals(2, mManager.getRemainingCallCount()); + + // Try stopping the user + mService.onCleanupUserInner(UserHandle.USER_SYSTEM); + + // Now it's unloaded. + assertEquals(1, mService.getShortcutsForTest().size()); + + // TODO Check all other fields + } +} diff --git a/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java b/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java new file mode 100644 index 000000000000..52e8f37f0b6c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.testutis; + +import android.test.MoreAsserts; + +import junit.framework.Assert; + +public class TestUtils { + private TestUtils() { + } + + public static void assertExpectException(Class<? extends Throwable> expectedExceptionType, + Runnable r) { + assertExpectException(expectedExceptionType, null, r); + } + + public static void assertExpectException(Class<? extends Throwable> expectedExceptionType, + String expectedExceptionMessageRegex, Runnable r) { + try { + r.run(); + Assert.fail("Expected exception type " + expectedExceptionType.getClass().getName() + + " was not thrown"); + } catch (Throwable e) { + Assert.assertTrue( + "Expected exception type was " + expectedExceptionType.getClass().getName() + + " but caught " + e.getClass().getName(), + expectedExceptionType.isAssignableFrom(e.getClass())); + if (expectedExceptionMessageRegex != null) { + MoreAsserts.assertContainsRegex(expectedExceptionMessageRegex, e.getMessage()); + } + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index e05f00d646f0..40687b01ceec 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -171,7 +171,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { // Fetch a ModelData instance from the hash map. Creates a new one if none // exists. - ModelData modelData = getOrCreateGenericModelData(modelId); + ModelData modelData = getOrCreateGenericModelDataLocked(modelId); IRecognitionStatusCallback oldCallback = modelData.getCallback(); if (oldCallback != null) { @@ -373,7 +373,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { // Also clear the internal state once the recognition has been stopped. modelData.setLoaded(); modelData.clearCallback(); - if (!computeRecognitionRunning()) { + if (!computeRecognitionRunningLocked()) { internalClearGlobalStateLocked(); } return status; @@ -505,12 +505,12 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { if (modelId == null || mModule == null) { return STATUS_ERROR; } - ModelData modelData = mGenericModelDataMap.get(modelId); - if (modelData == null) { - Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" + modelId); - return STATUS_ERROR; - } synchronized (mLock) { + ModelData modelData = mGenericModelDataMap.get(modelId); + if (modelData == null) { + Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" + modelId); + return STATUS_ERROR; + } if (!modelData.isModelLoaded()) { // Nothing to do here. Slog.i(TAG, "Unload: Given generic model is not loaded:" + modelId); @@ -530,7 +530,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { Slog.w(TAG, "unloadGenericSoundModel() force-marking model as unloaded."); } mGenericModelDataMap.remove(modelId); - if (DBG) dumpGenericModelState(); + if (DBG) dumpGenericModelStateLocked(); return status; } } @@ -580,7 +580,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) { return; } - ModelData model = getModelDataFor(event.soundModelHandle); + ModelData model = getModelDataForLocked(event.soundModelHandle); if (model == null) { Slog.w(TAG, "Generic recognition event: Model does not exist for handle: " + event.soundModelHandle); @@ -919,7 +919,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { mIsPowerSaveMode = mPowerManager.isPowerSaveMode(); } - private ModelData getOrCreateGenericModelData(UUID modelId) { + private ModelData getOrCreateGenericModelDataLocked(UUID modelId) { ModelData modelData = mGenericModelDataMap.get(modelId); if (modelData == null) { modelData = new ModelData(modelId); @@ -932,7 +932,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { // 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) { + private ModelData getModelDataForLocked(int modelHandle) { // Fetch ModelData object corresponding to the model handle. for (ModelData model : mGenericModelDataMap.values()) { if (model.getHandle() == modelHandle) { @@ -988,7 +988,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } } - if (DBG) dumpGenericModelState(); + if (DBG) dumpGenericModelStateLocked(); return status; } @@ -1017,11 +1017,11 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } } - if (DBG) dumpGenericModelState(); + if (DBG) dumpGenericModelStateLocked(); return status; } - private void dumpGenericModelState() { + private void dumpGenericModelStateLocked() { for (UUID modelId : mGenericModelDataMap.keySet()) { ModelData modelData = mGenericModelDataMap.get(modelId); Slog.i(TAG, "Model :" + modelData.toString()); @@ -1030,28 +1030,24 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { // 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) { + private boolean computeRecognitionRunningLocked() { + 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.isModelStarted()) { mRecognitionRunning = true; return mRecognitionRunning; } - for (UUID modelId : mGenericModelDataMap.keySet()) { - ModelData modelData = mGenericModelDataMap.get(modelId); - if (modelData.isModelStarted()) { - mRecognitionRunning = true; - return mRecognitionRunning; - } - } - mRecognitionRunning = false; } + mRecognitionRunning = false; return mRecognitionRunning; } diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java index 4bed94148599..ed7351f85a7a 100644 --- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java +++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java @@ -160,7 +160,7 @@ public class AppLaunch extends InstrumentationTestCase { for (String pair : appNames) { String[] parts = pair.split("\\^"); if (parts.length != 2) { - Log.e(TAG, "The apps key is incorectly formatted"); + Log.e(TAG, "The apps key is incorrectly formatted"); fail(); } @@ -176,6 +176,10 @@ public class AppLaunch extends InstrumentationTestCase { } } + private boolean hasLeanback(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); + } + private void createMappings() { mNameToIntent = new LinkedHashMap<String, Intent>(); mNameToProcess = new LinkedHashMap<String, String>(); @@ -183,9 +187,12 @@ public class AppLaunch extends InstrumentationTestCase { PackageManager pm = getInstrumentation().getContext() .getPackageManager(); Intent intentToResolve = new Intent(Intent.ACTION_MAIN); - intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); + intentToResolve.addCategory(hasLeanback(getInstrumentation().getContext()) ? + Intent.CATEGORY_LEANBACK_LAUNCHER : + Intent.CATEGORY_LAUNCHER); List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0); resolveLoop(ris, intentToResolve, pm); + // For Wear intentToResolve = new Intent(WEARABLE_ACTION_GOOGLE); ris = pm.queryIntentActivities(intentToResolve, 0); resolveLoop(ris, intentToResolve, pm); @@ -232,7 +239,7 @@ public class AppLaunch extends InstrumentationTestCase { // report error if any of the following is true: // * launch thread is alive // * result is not null, but: - // * result is not START_SUCESS + // * result is not START_SUCCESS // * or in case of no force stop, result is not TASK_TO_FRONT either if (t.isAlive() || (result != null && ((result.result != ActivityManager.START_SUCCESS) diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_linear_clamp.xml b/tests/VectorDrawableTest/res/color/fill_gradient_linear_clamp.xml new file mode 100644 index 000000000000..6a24453c0198 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_linear_clamp.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90" + android:startColor="?android:attr/colorPrimary" + android:endColor="?android:attr/colorControlActivated" + android:centerColor="#00ff0000" + android:startX="0" + android:startY="0" + android:endX="50" + android:endY="50" + android:type="linear" + android:tileMode="clamp"> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_overlap_mirror.xml b/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_overlap_mirror.xml new file mode 100644 index 000000000000..d342bca32208 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_overlap_mirror.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90" + android:startColor="?android:attr/colorPrimary" + android:endColor="?android:attr/colorControlActivated" + android:centerColor="#f00" + android:startX="0" + android:startY="0" + android:endX="50" + android:endY="50" + android:type="linear" + android:tileMode="mirror"> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.4" android:color="#f00"/> + <item android:offset="0.4" android:color="#fff"/> + <item android:offset="0.9" android:color="?android:attr/colorControlActivated"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_repeat.xml b/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_repeat.xml new file mode 100644 index 000000000000..afb45aa2eebe --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_linear_item_repeat.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90" + android:startColor="?android:attr/colorPrimary" + android:endColor="?android:attr/colorControlActivated" + android:centerColor="#f00" + android:startX="0" + android:startY="0" + android:endX="50" + android:endY="50" + android:type="linear" + android:tileMode="repeat"> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.4" android:color="#fff"/> + <item android:offset="0.9" android:color="?android:attr/colorControlActivated"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_radial_clamp.xml b/tests/VectorDrawableTest/res/color/fill_gradient_radial_clamp.xml new file mode 100644 index 000000000000..64b32f6fba3f --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_radial_clamp.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerColor="#ff0000" + android:endColor="?android:attr/colorControlActivated" + android:centerX="300" + android:centerY="300" + android:gradientRadius="50" + android:startColor="?android:attr/colorPrimary" + android:type="radial" + android:tileMode="clamp"> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item.xml b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item.xml index 51b0e173f746..c6cea7c5c698 100644 --- a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item.xml +++ b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- +<!-- /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_repeat.xml b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_repeat.xml new file mode 100644 index 000000000000..fb4346ad4abd --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_repeat.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerColor="#ff0000" + android:endColor="#ff0000ff" + android:centerX="300" + android:centerY="300" + android:gradientRadius="50" + android:startColor="#ffffffff" + android:type="radial" + android:tileMode="repeat"> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.4" android:color="#fff"/> + <item android:offset="0.9" android:color="?android:attr/colorControlActivated"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short.xml b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short.xml index 8caa1b4348a2..fefbe9f05eff 100644 --- a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short.xml +++ b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- +<!-- /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short_mirror.xml b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short_mirror.xml new file mode 100644 index 000000000000..8b5ad7c826ac --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_radial_item_short_mirror.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerX="300" + android:centerY="300" + android:gradientRadius="50" + android:type="radial" + android:tileMode="mirror"> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.9" android:color="?android:attr/colorControlActivated"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_sweep_clamp.xml b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_clamp.xml new file mode 100644 index 000000000000..80f39f3ee980 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_clamp.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerColor="#ff0000" + android:endColor="#ff0000ff" + android:centerX="500" + android:centerY="500" + android:gradientRadius="10" + android:startColor="#ffffffff" + android:type="sweep" + android:tileMode="clamp"> +</gradient> diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_long_mirror.xml b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_long_mirror.xml new file mode 100644 index 000000000000..0890bd6fc733 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_long_mirror.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerX="500" + android:centerY="500" + android:gradientRadius="10" + android:type="sweep" + android:tileMode="mirror"> + <item android:offset="-0.3" android:color="#f00"/> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.4" android:color="#0f0"/> + <item android:offset="0.6" android:color="#00f"/> + <item android:offset="0.7" android:color="?android:attr/colorControlActivated"/> + <item android:offset="1.5" android:color="#00f"/> +</gradient> diff --git a/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_repeat.xml b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_repeat.xml new file mode 100644 index 000000000000..2ec50148b44d --- /dev/null +++ b/tests/VectorDrawableTest/res/color/fill_gradient_sweep_item_repeat.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:centerColor="#ff0000" + android:endColor="#ff0000ff" + android:centerX="500" + android:centerY="500" + android:gradientRadius="10" + android:startColor="#ffffffff" + android:type="sweep" + android:tileMode="repeat"> + <item android:offset="0.1" android:color="?android:attr/colorPrimary"/> + <item android:offset="0.4" android:color="#fff"/> + <item android:offset="0.9" android:color="?android:attr/colorControlActivated"/> +</gradient> diff --git a/tests/VectorDrawableTest/res/color/stroke_gradient_clamp.xml b/tests/VectorDrawableTest/res/color/stroke_gradient_clamp.xml new file mode 100644 index 000000000000..3d746e720cf8 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/stroke_gradient_clamp.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90" + android:centerColor="#7f7f7f" + android:endColor="#ffffff" + android:startColor="#000000" + android:startX="0" + android:endX="50" + android:startY="0" + android:endY="0" + android:type="linear" + android:tileMode="clamp"> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/stroke_gradient_item_alpha_mirror.xml b/tests/VectorDrawableTest/res/color/stroke_gradient_item_alpha_mirror.xml new file mode 100644 index 000000000000..352a2fd463a8 --- /dev/null +++ b/tests/VectorDrawableTest/res/color/stroke_gradient_item_alpha_mirror.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:startX="0" + android:endX="50" + android:startY="0" + android:endY="0" + android:type="linear" + android:tileMode="mirror"> + <item android:offset="0.1" android:color="#f00"/> + <item android:offset="0.2" android:color="#2f0f"/> + <item android:offset="0.9" android:color="#f00f"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/color/stroke_gradient_item_repeat.xml b/tests/VectorDrawableTest/res/color/stroke_gradient_item_repeat.xml new file mode 100644 index 000000000000..42281d15dc0b --- /dev/null +++ b/tests/VectorDrawableTest/res/color/stroke_gradient_item_repeat.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90" + android:centerColor="#7f7f7f" + android:endColor="#ffffff" + android:startColor="#000000" + android:startX="0" + android:endX="50" + android:startY="0" + android:endY="0" + android:type="linear" + android:tileMode="repeat"> + <item android:offset="0.1" android:color="#f00"/> + <item android:offset="0.2" android:color="#f0f"/> + <item android:offset="0.9" android:color="#f00f"/> +</gradient>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_1_clamp.xml b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_1_clamp.xml new file mode 100644 index 000000000000..2fa440a84cff --- /dev/null +++ b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_1_clamp.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="64dp" + android:width="64dp" + android:viewportHeight="400" + android:viewportWidth="400" > + +<group android:name="backgroundGroup" + android:scaleX="0.5" + android:scaleY="0.5"> + <path + android:name="background1" + android:fillColor="@color/fill_gradient_linear_clamp" + android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background2" + android:fillColor="@color/fill_gradient_radial_clamp" + android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background3" + android:fillColor="@color/fill_gradient_sweep_clamp" + android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" /> +</group> +<group + android:name="translateToCenterGroup" + android:translateX="50.0" + android:translateY="90.0" > + <path + android:name="twoLines" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_clamp" + android:strokeWidth="20" /> + + <group + android:name="rotationGroup" + android:pivotX="0.0" + android:pivotY="0.0" + android:rotation="-45.0"> + <path + android:name="twoLines1" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_clamp" + android:strokeWidth="20" /> + + <group + android:name="translateGroup" + android:translateX="130.0" + android:translateY="160.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines3" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_clamp" + android:strokeWidth="20" /> + </group> + </group> + + <group + android:name="translateGroupHalf" + android:translateX="65.0" + android:translateY="80.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines2" + android:pathData="@string/twoLinePathData" + android:fillColor="@color/fill_gradient_linear_clamp" + android:strokeColor="@color/stroke_gradient_clamp" + android:strokeWidth="20" /> + </group> + </group> + </group> +</group> + +</vector>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_2_repeat.xml b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_2_repeat.xml new file mode 100644 index 000000000000..5a43f804a6e0 --- /dev/null +++ b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_2_repeat.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="64dp" + android:width="64dp" + android:viewportHeight="400" + android:viewportWidth="400" > + +<group android:name="backgroundGroup" + android:scaleX="0.5" + android:scaleY="0.5"> + <path + android:name="background1" + android:fillColor="@color/fill_gradient_linear_item_repeat" + android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background2" + android:fillColor="@color/fill_gradient_radial_item_repeat" + android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background3" + android:fillColor="@color/fill_gradient_sweep_item_repeat" + android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" /> +</group> +<group + android:name="translateToCenterGroup" + android:translateX="50.0" + android:translateY="90.0" > + <path + android:name="twoLines" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_repeat" + android:strokeWidth="20" /> + + <group + android:name="rotationGroup" + android:pivotX="0.0" + android:pivotY="0.0" + android:rotation="-45.0"> + <path + android:name="twoLines1" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_repeat" + android:strokeWidth="20" /> + + <group + android:name="translateGroup" + android:translateX="130.0" + android:translateY="160.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines3" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_repeat" + android:strokeWidth="20" /> + </group> + </group> + + <group + android:name="translateGroupHalf" + android:translateX="65.0" + android:translateY="80.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines2" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_repeat" + android:strokeWidth="20" /> + </group> + </group> + </group> +</group> + +</vector>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_3_mirror.xml b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_3_mirror.xml new file mode 100644 index 000000000000..e8de7c2b1f5d --- /dev/null +++ b/tests/VectorDrawableTest/res/drawable/vector_icon_gradient_3_mirror.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="64dp" + android:width="64dp" + android:viewportHeight="400" + android:viewportWidth="400" > + +<group android:name="backgroundGroup" + android:scaleX="0.5" + android:scaleY="0.5"> + <path + android:name="background1" + android:fillColor="@color/fill_gradient_linear_item_overlap_mirror" + android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background2" + android:fillColor="@color/fill_gradient_radial_item_short_mirror" + android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" /> + <path + android:name="background3" + android:fillColor="@color/fill_gradient_sweep_item_long_mirror" + android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" /> +</group> +<group + android:name="translateToCenterGroup" + android:translateX="50.0" + android:translateY="90.0" > + <path + android:name="twoLines" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_alpha_mirror" + android:strokeWidth="20" /> + + <group + android:name="rotationGroup" + android:pivotX="0.0" + android:pivotY="0.0" + android:rotation="-45.0"> + <path + android:name="twoLines1" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_alpha_mirror" + android:strokeWidth="20" /> + + <group + android:name="translateGroup" + android:translateX="130.0" + android:translateY="160.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines3" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_alpha_mirror" + android:strokeWidth="20" /> + </group> + </group> + + <group + android:name="translateGroupHalf" + android:translateX="65.0" + android:translateY="80.0"> + <group android:name="scaleGroup" > + <path + android:name="twoLines2" + android:pathData="@string/twoLinePathData" + android:strokeColor="@color/stroke_gradient_item_alpha" + android:strokeWidth="20" /> + </group> + </group> + </group> +</group> + +</vector>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/src/com/android/test/dynamic/VectorDrawablePerformance.java b/tests/VectorDrawableTest/src/com/android/test/dynamic/VectorDrawablePerformance.java index 717214740698..495d62010fb8 100644 --- a/tests/VectorDrawableTest/src/com/android/test/dynamic/VectorDrawablePerformance.java +++ b/tests/VectorDrawableTest/src/com/android/test/dynamic/VectorDrawablePerformance.java @@ -38,6 +38,9 @@ public class VectorDrawablePerformance extends Activity { R.drawable.vector_icon_gradient_1, R.drawable.vector_icon_gradient_2, R.drawable.vector_icon_gradient_3, + R.drawable.vector_icon_gradient_1_clamp, + R.drawable.vector_icon_gradient_2_repeat, + R.drawable.vector_icon_gradient_3_mirror, R.drawable.vector_icon_state_list_simple, R.drawable.vector_icon_state_list_theme, R.drawable.vector_drawable01, |