Merge "Camera2: Handle binder death on ICameraDeviceUser" into nyc-dev
diff --git a/Android.mk b/Android.mk
index ffc0421..08f3468 100644
--- a/Android.mk
+++ b/Android.mk
@@ -149,6 +149,7 @@
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 @@
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 5ec93cb..cc386db 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5885,7 +5885,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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.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 @@
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 @@
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 115224c..2f55373 100644
--- a/api/removed.txt
+++ b/api/removed.txt
@@ -43,6 +43,14 @@
}
+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.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 4224792..7823b3c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -6031,7 +6031,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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.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 @@
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 9ebc484..79f7297 100644
--- a/api/system-removed.txt
+++ b/api/system-removed.txt
@@ -41,6 +41,14 @@
}
+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 894971b..a478793 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5889,7 +5889,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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.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 @@
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 @@
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 115224c..2f55373 100644
--- a/api/test-removed.txt
+++ b/api/test-removed.txt
@@ -43,6 +43,14 @@
}
+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.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 1f1f318..cd4ace6 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -42,7 +42,7 @@
* @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 d8f0ac5..a2cc6b1 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -47,6 +47,7 @@
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 @@
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 @@
}
/**
+ * 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 16aee78..7cfa0cd 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -259,8 +259,7 @@
}
}
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 b1c5fd8..3a5dd30 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -37,7 +37,9 @@
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 @@
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 c4037f8..8bd40dc 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -5542,14 +5542,15 @@
/**
* 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 b7a16aa..dc73e26 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -262,7 +262,7 @@
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 b935b25..f96ddf0 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2680,6 +2680,7 @@
RADIO_SERVICE,
HARDWARE_PROPERTIES_SERVICE,
//@hide: SOUND_TRIGGER_SERVICE,
+ SHORTCUT_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -3576,6 +3577,14 @@
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 cc266c5..cf3e298 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.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 @@
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 1303696..e6525af 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 @@
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 0000000..23e671d
--- /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 e443d50..8e4a0e2 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 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 @@
}
}
+ /**
+ * 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 @@
}
}
}
+
+ @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 @@
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 @@
String packageName;
boolean replacing;
UserHandle user;
+ List<ShortcutInfo> shortcuts;
}
public CallbackMessageHandler(Looper looper, LauncherApps.Callback callback) {
@@ -527,6 +756,9 @@
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 @@
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/core/java/android/content/pm/ShortcutInfo.aidl b/core/java/android/content/pm/ShortcutInfo.aidl
new file mode 100644
index 0000000..08e1873
--- /dev/null
+++ b/core/java/android/content/pm/ShortcutInfo.aidl
@@ -0,0 +1,18 @@
+/*
+ * 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;
+
+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 0000000..6520563
--- /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 0000000..4c51d49
--- /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 0000000..3d6028a
--- /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 98ef2ea..cc46cbd 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.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 @@
// 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 @@
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 @@
mEndColor = copy.mEndColor;
mHasCenterColor = copy.mHasCenterColor;
mGradientRadius = copy.mGradientRadius;
+ mTileMode = copy.mTileMode;
if (copy.mItemColors != null) {
mItemColors = copy.mItemColors.clone();
@@ -117,6 +157,20 @@
}
}
+ // 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 @@
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 @@
}
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 @@
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 ee7bd9a..5c71373 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -235,6 +235,12 @@
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 ea180b2..f36bb29 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -24,9 +24,6 @@
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 @@
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 @@
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 ff37042..9467c22 100644
--- a/core/java/android/preference/EditTextPreference.java
+++ b/core/java/android/preference/EditTextPreference.java
@@ -49,6 +49,7 @@
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 @@
* @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 0439fe2..def303a 100644
--- a/core/java/android/provider/BlockedNumberContract.java
+++ b/core/java/android/provider/BlockedNumberContract.java
@@ -29,11 +29,14 @@
* <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 11737c6..0163b47 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 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 @@
@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 @@
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 @@
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 @@
.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 @@
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 @@
return 0;
}
+ @SystemApi
public Condition copy() {
final Parcel parcel = Parcel.obtain();
try {
@@ -186,10 +210,14 @@
}
}
+ @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 adcc9d6..44c3887 100644
--- a/core/java/android/service/notification/ConditionProviderService.java
+++ b/core/java/android/service/notification/ConditionProviderService.java
@@ -102,9 +102,6 @@
*/
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 4afa9fe..316c7e3 100644
--- a/core/java/android/transition/Transition.java
+++ b/core/java/android/transition/Transition.java
@@ -41,12 +41,12 @@
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 @@
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 @@
// 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 @@
}
}
}
- 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 6e02516..17f1991 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -135,6 +135,18 @@
}
/**
+ * 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 @@
@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 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 471b6d4..fc17f7a 100644
--- a/core/java/android/view/textservice/SpellCheckerInfo.java
+++ b/core/java/android/view/textservice/SpellCheckerInfo.java
@@ -31,10 +31,12 @@
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 @@
/**
* 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 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 90a573b..9ccd7f0 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8170,6 +8170,8 @@
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 1d7034d..17afd92 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 0120128..cad0e7b 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 0000000..7e72e25
--- /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 70995ac..9a9a89b 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -227,15 +227,16 @@
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 9595a85..c809ff4 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 @@
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 @@
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 6f23705..28ac116 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -180,8 +180,6 @@
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/CrashHandlerInjector.cpp b/libs/hwui/tests/unit/CrashHandlerInjector.cpp
deleted file mode 100644
index b1c678d..0000000
--- a/libs/hwui/tests/unit/CrashHandlerInjector.cpp
+++ /dev/null
@@ -1,41 +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.
- */
-
-#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;
diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp
new file mode 100644
index 0000000..409a12d
--- /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 b8f0717..3771474 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -1738,7 +1738,10 @@
// 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 c48bfc5..2aac2b3 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -2794,7 +2794,10 @@
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 c08f4bf..81cc035 100644
--- a/media/java/android/media/ImageReader.java
+++ b/media/java/android/media/ImageReader.java
@@ -335,7 +335,6 @@
switch (status) {
case ACQUIRE_SUCCESS:
- si.createSurfacePlanes();
si.mIsImageValid = true;
case ACQUIRE_NO_BUFS:
case ACQUIRE_MAX_IMAGES:
@@ -693,7 +692,7 @@
width = ImageReader.this.getWidth();
break;
default:
- width = nativeGetWidth(mFormat);
+ width = nativeGetWidth();
}
return width;
}
@@ -709,7 +708,7 @@
height = ImageReader.this.getHeight();
break;
default:
- height = nativeGetHeight(mFormat);
+ height = nativeGetHeight();
}
return height;
}
@@ -729,6 +728,10 @@
@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 @@
}
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 @@
}
}
- 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 @@
mBuffer = null;
}
- final private int mIndex;
final private int mPixelStride;
final private int mRowStride;
@@ -860,10 +856,10 @@
// 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 851d436..83a4f17 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -748,8 +748,8 @@
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 @@
}
- // 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 a332195..e8c50e3 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -575,16 +575,11 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 8fb58b5..bc20c17 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -491,7 +491,7 @@
* 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 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 @@
}
/**
- * Calls {@link #onTune}.
+ * Calls {@link #onTune(Uri, Bundle)}.
*/
void tune(Uri channelUri, Bundle params) {
mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
@@ -1836,7 +1837,7 @@
* 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 5c4b528..9623076 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -18,6 +18,7 @@
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 @@
* 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
*
* @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 @@
* @hide
*/
@SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
public void unblockContent(TvContentRating unblockedRating) {
if (mSession != null) {
mSession.unblockContent(unblockedRating);
@@ -539,7 +550,7 @@
}
/**
- * 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 @@
/**
* 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 9e90a19..c3993ae 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 {
jfieldID mNativeBuffer;
jfieldID mTimestamp;
+ jfieldID mPlanes;
} gSurfaceImageClassInfo;
static struct {
@@ -89,21 +85,12 @@
virtual void onFrameAvailable(const BufferItem& item);
- CpuConsumer::LockedBuffer* getLockedBuffer();
- void returnLockedBuffer(CpuConsumer::LockedBuffer* buffer);
+ BufferItem* getBufferItem();
+ void returnBufferItem(BufferItem* buffer);
- BufferItem* getOpaqueBuffer();
- void returnOpaqueBuffer(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 @@
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 @@
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 @@
}
}
-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 @@
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 @@
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 @@
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 @@
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 @@
return NULL;
}
- if (!ctx->isOpaque()) {
- jniThrowException(env, "java/lang/IllegalStateException",
- "Non-opaque ImageReader doesn't support this method");
- }
-
- return ctx->getOpaqueConsumer();
+ return ctx->getBufferConsumer();
}
-static BufferItem* Image_getOpaqueBuffer(JNIEnv* env, jobject image)
+static void Image_setBufferItem(JNIEnv* env, jobject thiz,
+ const BufferItem* buffer)
+{
+ env->SetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer, reinterpret_cast<jlong>(buffer));
+}
+
+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 @@
"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 @@
// 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 @@
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 @@
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 @@
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,156 +463,18 @@
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());
- }
-}
-
-static jint ImageReader_opaqueImageSetup(JNIEnv* env, JNIImageReaderContext* ctx, jobject image) {
- ALOGV("%s:", __FUNCTION__);
- if (ctx == NULL || !ctx->isOpaque()) {
- jniThrowRuntimeException(env, "ImageReaderContext is not initialized");
- return -1;
+ BufferItemConsumer* bufferConsumer = ctx->getBufferConsumer();
+ BufferItem* buffer = Image_getBufferItem(env, image);
+ if (buffer == nullptr) {
+ // Release an already closed image is harmless.
+ return;
}
- BufferItemConsumer* opaqueConsumer = ctx->getOpaqueConsumer();
- BufferItem* buffer = ctx->getOpaqueBuffer();
- 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);
- 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) {
- 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);
- }
- }
- 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;
- }
-
- // 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 = 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);
- env->SetLongField(image, gSurfaceImageClassInfo.mTimestamp,
- static_cast<jlong>(buffer->timestamp));
-
- return ACQUIRE_SUCCESS;
+ 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_imageSetup(JNIEnv* env, jobject thiz, jobject image) {
@@ -1103,11 +486,99 @@
return -1;
}
- if (ctx->isOpaque()) {
- return ImageReader_opaqueImageSetup(env, ctx, image);
- } else {
- return ImageReader_lockedImageSetup(env, ctx, image);
+ 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 = bufferConsumer->acquireBuffer(buffer, 0);
+ if (res != OK) {
+ 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: 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;
+ }
+
+ // 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 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;
+ }
+ }
+
+ }
+
+ // Set SurfaceImage instance member variables
+ Image_setBufferItem(env, image, buffer);
+ env->SetLongField(image, gSurfaceImageClassInfo.mTimestamp,
+ static_cast<jlong>(buffer->mTimestamp));
+
+ return ACQUIRE_SUCCESS;
}
static jint ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image) {
@@ -1118,29 +589,23 @@
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 @@
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 @@
return android_view_Surface_createFromIGraphicBufferProducer(env, gbp);
}
-static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int readerFormat)
+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",
+ "Image is not initialized");
+ return;
+ }
+
+ 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;
+ }
+
+ // 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.
+}
+
+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__);
+
+ 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,
+ int numPlanes, int readerFormat)
{
- int rowStride, pixelStride;
+ 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;
+
PublicFormat publicReaderFormat = static_cast<PublicFormat>(readerFormat);
int halReaderFormat = android_view_Surface_mapPublicFormatToHalFormat(
publicReaderFormat);
- ALOGV("%s: buffer index: %d", __FUNCTION__, idx);
+ 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;
+ }
+
+ 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)) {
- jniThrowException(env, "java/lang/IllegalStateException",
- "Opaque images from Opaque ImageReader do not have any planes");
- return NULL;
+ // Return 0 element surface array.
+ return surfacePlanes;
}
- CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz);
+ 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;
+ }
- ALOG_ASSERT(buffer != NULL);
- if (buffer == NULL) {
- jniThrowException(env, "java/lang/IllegalStateException", "Image was released");
+ // Finally, create this SurfacePlane.
+ jobject surfacePlane = env->NewObject(gSurfacePlaneClassInfo.clazz,
+ gSurfacePlaneClassInfo.ctor, thiz, rowStride, pixelStride, byteBuffer);
+ env->SetObjectArrayElement(surfacePlanes, i, surfacePlane);
}
- rowStride = Image_imageGetRowStride(env, buffer, idx, halReaderFormat);
- pixelStride = Image_imageGetPixelStride(env, buffer, idx, halReaderFormat);
-
- jobject surfPlaneObj = env->NewObject(gSurfacePlaneClassInfo.clazz,
- gSurfacePlaneClassInfo.ctor, thiz, idx, rowStride, pixelStride);
-
- return surfPlaneObj;
+ return surfacePlanes;
}
-static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int readerFormat)
+static jint Image_getWidth(JNIEnv* env, jobject thiz)
{
- 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: buffer index: %d", __FUNCTION__, idx);
-
- if (isFormatOpaque(readerHalFormat)) {
- jniThrowException(env, "java/lang/IllegalStateException",
- "Opaque images from Opaque ImageReader do not have any plane");
- 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);
- 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");
- }
-
- return byteBuffer;
+ BufferItem* buffer = Image_getBufferItem(env, thiz);
+ return getBufferWidth(buffer);
}
-static jint Image_getWidth(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_getOpaqueBufferWidth(opaqueBuffer);
- } else {
- CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz);
- return Image_getBufferWidth(buffer);
- }
-}
-
-static jint Image_getHeight(JNIEnv* env, jobject thiz, jint format)
-{
- 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 @@
// 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 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 f50da85..d5d9fc9 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 @@
jmethodID ctor;
} gSurfacePlaneClassInfo;
-typedef CpuConsumer::LockedBuffer LockedImage;
-
// ----------------------------------------------------------------------------
class JNIImageWriterContext : public BnProducerListener {
@@ -181,13 +173,11 @@
// -------------------------------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 @@
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 @@
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 @@
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;
+ // 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;
}
- // 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;
- }
- }
-
- 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;
+ 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);
}
-
- *base = pData;
- *size = dataSize;
- *pixelStride = pStride;
- *rowStride = rStride;
}
static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz,
@@ -1024,7 +757,8 @@
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 @@
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 9c4f7c4..f429661 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 @@
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 a30e1be..3791ec9 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 @@
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 7c1142b..7e438a1 100644
--- a/media/tests/MediaFrameworkTest/Android.mk
+++ b/media/tests/MediaFrameworkTest/Android.mk
@@ -11,7 +11,6 @@
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 990ce0b..a1c2910 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 79f6fa9..2aee569 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 fa11244..1b67ee5 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 b5d1150..982b204 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 9fc8a73..5af7da3 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 6e1b30e..e2d1870 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 6efe9d1..c9d18b3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -154,7 +154,7 @@
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 @@
return true;
case R.id.menu_search:
+ // SearchViewManager listens for this directly.
return false;
case R.id.menu_sort_name:
@@ -366,6 +367,7 @@
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 0000000..bae2d58
--- /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 9fceff5..020f2c0 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 @@
*/
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 @@
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 @@
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 1967923..ca1b444 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.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 @@
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 @@
@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 @@
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 @@
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 @@
}.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 @@
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 5f33b32..9a06807 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 @@
device.pressBack();
}
+ @Suppress
public void testRenameFile_OkButton() throws Exception {
bots.directory.selectDocument(fileName1);
bots.main.openOverflowMenu();
@@ -101,6 +101,7 @@
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 ae2c6e7..ef397f6 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 4c0450e..0000000
--- 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 51d8ca0..978ca94 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 a424d55..987b5ea 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 @@
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 @@
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 @@
}
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 @@
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 0fad113..1c6a071 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.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 @@
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 @@
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 @@
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 @@
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 8b337ea..c1fe1a8 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 86bea87..bad739fd 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 @@
}
@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 dba731a..526e5fa 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 @@
}
@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 89d20de..cb761a9 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 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 299d0e3..610e219 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 @@
}
@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 8924694..77fda20 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 @@
}
@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 78bc0dd..de8a188 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 @@
}
@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 652d969..9b6ddc8 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 @@
}
@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 0000000..1338d9d
--- /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 c09376b..937f7d3 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 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 @@
}
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 @@
}
private void onSessionStart() {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onSessionStart", "classifierEnabled=" + isClassiferEnabled());
+ }
mBouncerOn = false;
mSessionActive = true;
@@ -154,6 +165,16 @@
* @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 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 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 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 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 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 void onQsDown() {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onQsDown", "");
+ }
mHumanInteractionClassifier.setType(Classifier.QUICK_SETTINGS);
mDataCollector.onQsDown();
}
@@ -230,6 +288,9 @@
}
public void onTrackingStarted() {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onTrackingStarted", "");
+ }
mHumanInteractionClassifier.setType(Classifier.UNLOCK);
mDataCollector.onTrackingStarted();
}
@@ -251,6 +312,9 @@
}
public void onNotificatonStartDraggingDown() {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onNotificatonStartDraggingDown", "");
+ }
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DRAG_DOWN);
mDataCollector.onNotificatonStartDraggingDown();
}
@@ -264,6 +328,9 @@
}
public void onNotificatonStartDismissing() {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onNotificatonStartDismissing", "");
+ }
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
mDataCollector.onNotificatonStartDismissing();
}
@@ -281,6 +348,9 @@
}
public void onAffordanceSwipingStarted(boolean rightCorner) {
+ if (FalsingLog.ENABLED) {
+ FalsingLog.i("onAffordanceSwipingStarted", "");
+ }
if (rightCorner) {
mHumanInteractionClassifier.setType(Classifier.RIGHT_AFFORDANCE);
} else {
@@ -311,4 +381,14 @@
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 45eb9ad..5e35d76 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.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 @@
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 @@
// 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));
+ 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.add(new PointerCountClassifier(mClassifierData));
- mGestureClassifiers.add(new ProximityClassifier(mClassifierData));
-
- mStrokeClassifiersSize = mStrokeClassifiers.size();
- mGestureClassifiersSize = mGestureClassifiers.size();
+ mGestureClassifiers = new GestureClassifier[] {
+ new PointerCountClassifier(mClassifierData),
+ new ProximityClassifier(mClassifierData)
+ };
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(HIC_ENABLE), false,
@@ -150,21 +151,30 @@
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 @@
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 @@
@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 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 cedf467..53678a6 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 @@
}
@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 5097b63..136c433 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 @@
}
@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 6995064..62adfc8 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 @@
}
@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 d58274d..6df72b1 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 @@
}
@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 81b78c7..01fcc37 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 @@
}
@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 6137349..c8c71cb 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 35ade58..db17245 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 @@
}
}
+ 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 @@
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 @@
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 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 @@
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 0709992..db686a8 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 @@
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 @@
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 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 c4436f4..2aad161 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 @@
mHandler = handler;
mIntent = intent;
mUser = user;
+ if (DEBUG) Log.d(TAG, "Creating " + mIntent + " " + mUser);
}
public ComponentName getComponent() {
@@ -116,13 +117,13 @@
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 @@
@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 ac23f44..3ad2cc3 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 @@
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 1af5a55..8342de5 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 @@
// 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 9da8fee..2601cd3 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 @@
/**
* 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 @@
// 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 a06700d..e64354c 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.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 @@
private Bitmap mScreenBitmap;
private View mScreenshotLayout;
+ private ScreenshotSelectorView mScreenshotSelectorView;
private ImageView mBackgroundView;
private ImageView mScreenshotView;
private ImageView mScreenshotFlash;
@@ -437,7 +439,11 @@
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 @@
// 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 @@
/**
* 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 @@
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 @@
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 0000000..07a9246
--- /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 456b5fa..4badc42 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.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 @@
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 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 7e7fc3a..c0148c0 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 @@
* 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 9c6f2c5..d50e67a 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 @@
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 @@
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 @@
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 7856f6c..6ad7aad 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.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 @@
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 @@
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 @@
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 f894a22..5dcd393 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 @@
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 @@
}
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 @@
}
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 @@
}
}
}
+ 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 1a557e4..3eda320 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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
} 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 @@
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 @@
&& mOnAnimationFinished != null) {
mOnAnimationFinished.run();
mOnAnimationFinished = null;
+ mKeyguardFadingOutInProgress = false;
}
}
@@ -406,6 +428,10 @@
}
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 0e84f733..2ba1562 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 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 cca3746..824b8b7 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 @@
// 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 123165e..ea15b81 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.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 @@
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 4877a378..4e667c6 100644
--- a/rs/jni/android_renderscript_RenderScript.cpp
+++ b/rs/jni/android_renderscript_RenderScript.cpp
@@ -1062,7 +1062,7 @@
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 @@
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 @@
}
return (jlong)(uintptr_t)rsTypeCreate((RsContext)con, (RsElement)eid, dimx, dimy, dimz, mips,
- faces, yuv, true);
+ faces, yuv);
}
static void
@@ -1211,7 +1211,7 @@
}
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 @@
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 @@
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 @@
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 9e6c21c..b4b40ae 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 @@
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 3f453dc..306e933 100644
--- a/services/core/java/com/android/server/TextServicesManagerService.java
+++ b/services/core/java/com/android/server/TextServicesManagerService.java
@@ -27,6 +27,7 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
}
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 @@
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 @@
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 @@
private int[] mCurrentProfileIds = new int[0];
private Object mLock = new Object();
- public TextServicesSettings(ContentResolver resolver, @UserIdInt int 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 TextServicesSettings(ContentResolver resolver, @UserIdInt int userId,
+ boolean copyOnWrite) {
mResolver = resolver;
- mCurrentUserId = userId;
+ switchCurrentUser(userId, copyOnWrite);
}
- public void setCurrentUserId(@UserIdInt int userId) {
+ /**
+ * 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 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 0317641..f07c1d0 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 @@
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 @@
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 2ea5d15..5219827 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 @@
}
}
- 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 e50722a..39895ac5 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 @@
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 @@
"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 62275a9..ea85fa1 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.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_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 @@
// 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 @@
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 addffd3..4a5df7a 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_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.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.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 @@
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 @@
mUserLru.add(UserHandle.USER_SYSTEM);
mLockPatternUtils = new LockPatternUtils(mService.mContext);
updateStartedUserArrayLocked();
- mUserSetupCompleteContentObserver = new UserSetupCompleteContentObserver(mHandler);
}
void finishUserSwitch(UserState uss) {
@@ -477,7 +442,6 @@
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 Integer userIdInt = userId;
mUserLru.remove(userIdInt);
mUserLru.add(userIdInt);
- updateUserSetupCompleteLocked(userId);
if (foreground) {
mCurrentUserId = userId;
@@ -892,22 +855,6 @@
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 @@
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 fba7e7d..d2b77aa 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.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 @@
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 0000000..2d62c1c
--- /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 98bf8a9..4a2c88c 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 @@
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 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 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 @@
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 9820a12..c19b51f 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 @@
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 @@
}
}
}
- 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 @@
}
}
- 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 @@
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 8d75f60..5612870 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.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.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 @@
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 @@
}
}
+ 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 @@
}
}
+ 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 @@
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 @@
}
}
+ 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 @@
}
- 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 @@
}
}
+ // TODO Simplify with lambdas.
+
@Override
public void onPackageAdded(String packageName, int uid) {
UserHandle user = new UserHandle(getChangingUserId());
@@ -523,6 +620,25 @@
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 0000000..423767a
--- /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 e51a2e1..1603f1c 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.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 @@
+ SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
mScreenshotChordVolumeDownKeyConsumed = true;
cancelPendingPowerKeyAction();
-
+ mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
}
}
@@ -1269,12 +1271,20 @@
}
};
- 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 @@
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 @@
}
}
}
+ } 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 @@
}
} else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
if (down && repeatCount == 0) {
+ mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mHandler.post(mScreenshotRunnable);
}
return -1;
@@ -3360,8 +3384,8 @@
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 @@
"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 @@
};
// 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 @@
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 e5c5b2bc..858f7c7 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 @@
private boolean mBound;
private long mScheduledRestartUptimeMillis;
private long mMaximumTimeToLock; // from DevicePolicyManager
+ private boolean mPendingSuccessfulUnlock = false;
// Trust state
private boolean mTrusted;
@@ -234,6 +235,11 @@
setCallback(mCallback);
updateDevicePolicyFeatures();
+ if (mPendingSuccessfulUnlock) {
+ onUnlockAttempt(true);
+ mPendingSuccessfulUnlock = false;
+ }
+
if (mTrustManagerService.isDeviceLockedInner(mUserId)) {
onDeviceLocked();
} else {
@@ -302,7 +308,11 @@
*/
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 b54e866..984fb76 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
if (trustMayHaveChanged) {
- if (userId == UserHandle.USER_ALL) {
+ if (userIdOrAll == UserHandle.USER_ALL) {
updateTrustAll();
} else {
- updateTrust(userId, 0);
+ updateTrust(userIdOrAll, 0);
}
}
}
@@ -578,6 +571,10 @@
}
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 @@
}
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 @@
}
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 @@
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 49aaa4a..30442bc 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 @@
@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 2731f429..9795c93 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 @@
boolean mAlwaysFocusable;
boolean mAppStopped;
+ int mPendingRelaunchCount;
ArrayDeque<Rect> mFrozenBounds = new ArrayDeque<>();
@@ -531,6 +532,26 @@
}
}
+ 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 @@
}
}
+ 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 @@
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 b64aaa8..a998bc3 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.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.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 @@
mTokenMap.remove(token.token);
} else if (atoken != null) {
atoken.firstWindowDrawn = false;
+ atoken.allDrawn = false;
}
}
@@ -4251,6 +4256,28 @@
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 @@
}
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 @@
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 @@
if (auditSafeMode >= buildDate) {
mSafeMode = true;
+ showAuditSafeModeNotification();
} else {
SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, "");
SystemProperties.set(ShutdownThread.AUDIT_SAFEMODE_PROPERTY, "");
@@ -9636,8 +9671,8 @@
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 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 18aa186..8ada2f1 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 @@
"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 d979675..4831db5 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.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 @@
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 @@
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 @@
* - 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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 68fd0f6..b316cbd 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 @@
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 a2d5259..59f8284a 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.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 @@
// 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 3ae1072..23f186c 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
new file mode 100644
index 0000000..64eb294
--- /dev/null
+++ b/services/tests/servicestests/res/drawable/icon1.png
Binary files differ
diff --git a/services/tests/servicestests/res/drawable/icon2.png b/services/tests/servicestests/res/drawable/icon2.png
new file mode 100644
index 0000000..7502484
--- /dev/null
+++ b/services/tests/servicestests/res/drawable/icon2.png
Binary files differ
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 8c47087..e897e3d 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 @@
// 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 @@
// 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 @@
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 0000000..eb16a1d
--- /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 0000000..1f805e9
--- /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 0000000..52e8f37
--- /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 e05f00d..40687b0 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 @@
// 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 @@
// 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 @@
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 @@
Slog.w(TAG, "unloadGenericSoundModel() force-marking model as unloaded.");
}
mGenericModelDataMap.remove(modelId);
- if (DBG) dumpGenericModelState();
+ if (DBG) dumpGenericModelStateLocked();
return status;
}
}
@@ -580,7 +580,7 @@
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 @@
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 @@
// 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 @@
}
}
}
- if (DBG) dumpGenericModelState();
+ if (DBG) dumpGenericModelStateLocked();
return status;
}
@@ -1017,11 +1017,11 @@
}
}
}
- 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 @@
// 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 4bed941..ed7351f8 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 @@
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 @@
}
}
+ 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 @@
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 @@
// 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 0000000..6a24453
--- /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 0000000..d342bca
--- /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 0000000..afb45aa
--- /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 0000000..64b32f6
--- /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 51b0e17..c6cea7c 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 0000000..fb4346a
--- /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 8caa1b4..fefbe9f 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 0000000..8b5ad7c
--- /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 0000000..80f39f3e
--- /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 0000000..0890bd6
--- /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 0000000..2ec5014
--- /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 0000000..3d746e7
--- /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 0000000..352a2fd
--- /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 0000000..42281d1
--- /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 0000000..2fa440a
--- /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 0000000..5a43f80
--- /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 0000000..e8de7c2
--- /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 7172147..495d620 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 @@
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,