Merge \\"Fix infinite loop when registering data usage callback.\\" into nyc-dev am: c20ffc2a9e
am: 13cf0e7d54
Change-Id: Idb7a28cf53e4c1b963df2dc0aff8de47d9ca8069
diff --git a/Android.mk b/Android.mk
index 27cf90c..4ac17b2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -694,6 +694,7 @@
frameworks/base/core/java/android/service/quicksettings/Tile.aidl \
frameworks/native/aidl/binder/android/os/PersistableBundle.aidl \
system/netd/server/binder/android/net/UidRange.aidl \
+ frameworks/base/telephony/java/android/telephony/PcoData.aidl \
gen := $(TARGET_OUT_COMMON_INTERMEDIATES)/framework.aidl
$(gen): PRIVATE_SRC_FILES := $(aidl_files)
diff --git a/api/current.txt b/api/current.txt
index 70939f8..a3b793d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -420,7 +420,9 @@
field public static final int contentInsetStart = 16843859; // 0x1010453
field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522
field public static final int contextClickable = 16844007; // 0x10104e7
+ field public static final int contextDescription = 16844078; // 0x101052e
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844077; // 0x101052d
field public static final int controlX1 = 16843772; // 0x10103fc
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
@@ -1039,6 +1041,7 @@
field public static final int rotation = 16843558; // 0x1010326
field public static final int rotationX = 16843559; // 0x1010327
field public static final int rotationY = 16843560; // 0x1010328
+ field public static final int roundIcon = 16844076; // 0x101052c
field public static final int rowCount = 16843637; // 0x1010375
field public static final int rowDelay = 16843216; // 0x10101d0
field public static final int rowEdgeFlags = 16843329; // 0x1010241
@@ -1106,11 +1109,16 @@
field public static final int shareInterpolator = 16843195; // 0x10101bb
field public static final int sharedUserId = 16842763; // 0x101000b
field public static final int sharedUserLabel = 16843361; // 0x1010261
+ field public static final int shortcutDisabledMessage = 16844075; // 0x101052b
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutLongLabel = 16844074; // 0x101052a
+ field public static final int shortcutShortLabel = 16844073; // 0x1010529
field public static final int shouldDisableView = 16843246; // 0x10101ee
field public static final int showAsAction = 16843481; // 0x10102d9
field public static final int showDefault = 16843258; // 0x10101fa
field public static final int showDividers = 16843561; // 0x1010329
field public static final int showForAllUsers = 16844015; // 0x10104ef
+ field public static final int showMetadataInPreview = 16844079; // 0x101052f
field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9
field public static final int showSilent = 16843259; // 0x10101fb
field public static final int showText = 16843949; // 0x10104ad
@@ -5037,12 +5045,14 @@
method public android.app.Notification.Action.Builder extend(android.app.Notification.Action.Builder);
method public java.lang.CharSequence getCancelLabel();
method public java.lang.CharSequence getConfirmLabel();
+ method public boolean getHintDisplayActionInline();
method public boolean getHintLaunchesActivity();
method public java.lang.CharSequence getInProgressLabel();
method public boolean isAvailableOffline();
method public android.app.Notification.Action.WearableExtender setAvailableOffline(boolean);
method public android.app.Notification.Action.WearableExtender setCancelLabel(java.lang.CharSequence);
method public android.app.Notification.Action.WearableExtender setConfirmLabel(java.lang.CharSequence);
+ method public android.app.Notification.Action.WearableExtender setHintDisplayActionInline(boolean);
method public android.app.Notification.Action.WearableExtender setHintLaunchesActivity(boolean);
method public android.app.Notification.Action.WearableExtender setInProgressLabel(java.lang.CharSequence);
}
@@ -5756,7 +5766,10 @@
method public android.content.pm.ServiceInfo getServiceInfo();
method public java.lang.String getServiceName();
method public java.lang.String getSettingsActivity();
+ method public boolean getShowMetadataInPreview();
method public java.lang.CharSequence loadAuthor(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public java.lang.CharSequence loadContextDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public android.net.Uri loadContextUri(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence loadDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
method public java.lang.CharSequence loadLabel(android.content.pm.PackageManager);
@@ -6484,11 +6497,13 @@
method public android.content.res.Configuration getConfiguration();
method public int getEventType();
method public java.lang.String getPackageName();
+ method public java.lang.String getShortcutId();
method public long getTimeStamp();
field public static final int CONFIGURATION_CHANGE = 5; // 0x5
field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
field public static final int NONE = 0; // 0x0
+ field public static final int SHORTCUT_INVOCATION = 8; // 0x8
field public static final int USER_INTERACTION = 7; // 0x7
}
@@ -8191,6 +8206,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 SYSTEM_HEALTH_SERVICE = "systemhealth";
field public static final java.lang.String TELECOM_SERVICE = "telecom";
@@ -9500,13 +9516,22 @@
public class LauncherApps {
method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle);
+ method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(java.lang.String, 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 hasShortcutHostPermission();
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 boolean startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
}
@@ -9519,6 +9544,20 @@
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 android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
+ method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long);
+ method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String);
+ method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int);
+ method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List<java.lang.String>);
+ 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_MANIFEST = 8; // 0x8
+ field public static final int FLAG_GET_PINNED = 2; // 0x2
}
public class PackageInfo implements android.os.Parcelable {
@@ -10019,6 +10058,74 @@
field public java.lang.String permission;
}
+ public final class ShortcutInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.content.ComponentName getActivity();
+ method public java.util.Set<java.lang.String> getCategories();
+ method public java.lang.CharSequence getDisabledMessage();
+ method public int getDisabledMessageResourceId();
+ method public android.os.PersistableBundle getExtras();
+ method public int getIconResourceId();
+ method public java.lang.String getId();
+ method public android.content.Intent getIntent();
+ method public long getLastChangedTimestamp();
+ method public java.lang.CharSequence getLongLabel();
+ method public int getLongLabelResourceId();
+ method public java.lang.String getPackage();
+ method public int getRank();
+ method public java.lang.CharSequence getShortLabel();
+ method public int getShortLabelResourceId();
+ method public android.os.UserHandle getUserHandle();
+ method public boolean hasIconFile();
+ method public boolean hasIconResource();
+ method public boolean hasKeyFieldsOnly();
+ method public boolean hasStringResourcesResolved();
+ method public boolean isDynamic();
+ method public boolean isEnabled();
+ method public boolean isImmutable();
+ method public boolean isManifestShortcut();
+ method public boolean isPinned();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
+ field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
+ }
+
+ 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 setActivity(android.content.ComponentName);
+ method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set<java.lang.String>);
+ method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence);
+ 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 setLongLabel(java.lang.CharSequence);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence);
+ }
+
+ public class ShortcutManager {
+ method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
+ method public void disableShortcuts(java.util.List<java.lang.String>);
+ method public void disableShortcuts(java.util.List<java.lang.String>, int);
+ method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.String);
+ method public void enableShortcuts(java.util.List<java.lang.String>);
+ method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+ method public int getIconMaxHeight();
+ method public int getIconMaxWidth();
+ method public java.util.List<android.content.pm.ShortcutInfo> getManifestShortcuts();
+ method public int getMaxShortcutCountForActivity();
+ method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
+ method public long getRateLimitResetTime();
+ method public int getRemainingCallCount();
+ method public void removeAllDynamicShortcuts();
+ method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
+ method public void reportShortcutUsed(java.lang.String);
+ 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);
@@ -30640,6 +30747,7 @@
public static class CallLog.Calls implements android.provider.BaseColumns {
ctor public CallLog.Calls();
method public static java.lang.String getLastOutgoingCall(android.content.Context);
+ field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7
field public static final int BLOCKED_TYPE = 6; // 0x6
field public static final java.lang.String CACHED_FORMATTED_NUMBER = "formatted_number";
field public static final java.lang.String CACHED_LOOKUP_URI = "lookup_uri";
@@ -30662,6 +30770,7 @@
field public static final java.lang.String DURATION = "duration";
field public static final java.lang.String EXTRA_CALL_TYPE_FILTER = "android.provider.extra.CALL_TYPE_FILTER";
field public static final java.lang.String FEATURES = "features";
+ field public static final int FEATURES_PULLED_EXTERNALLY = 2; // 0x2
field public static final int FEATURES_VIDEO = 1; // 0x1
field public static final java.lang.String GEOCODED_LOCATION = "geocoded_location";
field public static final int INCOMING_TYPE = 1; // 0x1
@@ -32281,6 +32390,7 @@
field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
+ field public static final java.lang.String ACTION_DELETION_HELPER_SETTINGS = "android.settings.DELETION_HELPER_SETTINGS";
field public static final java.lang.String ACTION_DEVICE_INFO_SETTINGS = "android.settings.DEVICE_INFO_SETTINGS";
field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
@@ -32984,6 +33094,7 @@
field public static final int QUOTA_UNAVAILABLE = -1; // 0xffffffff
field public static final java.lang.String SETTINGS_URI = "settings_uri";
field public static final java.lang.String SOURCE_PACKAGE = "source_package";
+ field public static final java.lang.String SOURCE_TYPE = "source_type";
field public static final java.lang.String VOICEMAIL_ACCESS_URI = "voicemail_access_uri";
}
@@ -35936,9 +36047,14 @@
method public void phoneAccountSelected(android.telecom.PhoneAccountHandle, boolean);
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
+ method public final void putExtras(android.os.Bundle);
method public void registerCallback(android.telecom.Call.Callback);
method public void registerCallback(android.telecom.Call.Callback, android.os.Handler);
method public void reject(boolean, java.lang.String);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
+ method public void sendCallEvent(java.lang.String, android.os.Bundle);
method public void splitFromConference();
method public void stopDtmfTone();
method public void swapConference();
@@ -35952,6 +36068,7 @@
field public static final int STATE_DISCONNECTING = 10; // 0xa
field public static final int STATE_HOLDING = 3; // 0x3
field public static final int STATE_NEW = 0; // 0x0
+ field public static final int STATE_PULLING_CALL = 11; // 0xb
field public static final int STATE_RINGING = 2; // 0x2
field public static final int STATE_SELECT_PHONE_ACCOUNT = 8; // 0x8
}
@@ -35962,6 +36079,7 @@
method public void onCannedTextResponsesLoaded(android.telecom.Call, java.util.List<java.lang.String>);
method public void onChildrenChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
method public void onConferenceableCallsChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public void onDetailsChanged(android.telecom.Call, android.telecom.Call.Details);
method public void onParentChanged(android.telecom.Call, android.telecom.Call);
method public void onPostDialWait(android.telecom.Call, java.lang.String);
@@ -35992,6 +36110,7 @@
method public static java.lang.String propertiesToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 8388608; // 0x800000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
field public static final int CAPABILITY_HOLD = 1; // 0x1
field public static final int CAPABILITY_MANAGE_CONFERENCE = 128; // 0x80
@@ -36011,7 +36130,9 @@
field public static final int PROPERTY_EMERGENCY_CALLBACK_MODE = 4; // 0x4
field public static final int PROPERTY_ENTERPRISE_CALL = 32; // 0x20
field public static final int PROPERTY_GENERIC_CONFERENCE = 2; // 0x2
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 128; // 0x80
field public static final int PROPERTY_HIGH_DEF_AUDIO = 16; // 0x10
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 64; // 0x40
field public static final int PROPERTY_WIFI = 8; // 0x8
}
@@ -36062,6 +36183,7 @@
method public final android.telecom.CallAudioState getCallAudioState();
method public final java.util.List<android.telecom.Connection> getConferenceableConnections();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final long getConnectionTime();
method public final java.util.List<android.telecom.Connection> getConnections();
method public final android.telecom.DisconnectCause getDisconnectCause();
@@ -36074,6 +36196,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onConnectionAdded(android.telecom.Connection);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onMerge(android.telecom.Connection);
method public void onMerge();
@@ -36082,14 +36205,18 @@
method public void onStopDtmfTone();
method public void onSwap();
method public void onUnhold();
+ method public final void putExtras(android.os.Bundle);
method public final void removeConnection(android.telecom.Connection);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
method public final void setActive();
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setConnectionTime(long);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setOnHold();
method public final void setStatusHints(android.telecom.StatusHints);
method public final void setVideoProvider(android.telecom.Connection, android.telecom.Connection.VideoProvider);
@@ -36115,6 +36242,7 @@
method public final android.telecom.Conference getConference();
method public final java.util.List<android.telecom.Conferenceable> getConferenceables();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public final int getState();
@@ -36125,16 +36253,24 @@
method public void onAnswer(int);
method public void onAnswer();
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+ method public void onCallEvent(java.lang.String, android.os.Bundle);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onPlayDtmfTone(char);
method public void onPostDialContinue(boolean);
+ method public void onPullExternalCall();
method public void onReject();
method public void onReject(java.lang.String);
method public void onSeparate();
method public void onStateChanged(int);
method public void onStopDtmfTone();
method public void onUnhold();
+ method public static java.lang.String propertiesToString(int);
+ method public final void putExtras(android.os.Bundle);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
+ method public void sendConnectionEvent(java.lang.String, android.os.Bundle);
method public final void setActive();
method public final void setAddress(android.net.Uri, int);
method public final void setAudioModeIsVoip(boolean);
@@ -36142,9 +36278,10 @@
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final void setConferenceables(java.util.List<android.telecom.Conferenceable>);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setInitialized();
method public final void setInitializing();
method public final void setNextPostDialChar(char);
@@ -36158,6 +36295,7 @@
method public static java.lang.String stateToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 8388608; // 0x800000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 16777216; // 0x1000000
field public static final int CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 524288; // 0x80000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
@@ -36175,15 +36313,20 @@
field public static final int CAPABILITY_SUPPORTS_VT_REMOTE_TX = 2048; // 0x800
field public static final int CAPABILITY_SUPPORT_HOLD = 2; // 0x2
field public static final int CAPABILITY_SWAP_CONFERENCE = 8; // 0x8
+ field public static final java.lang.String EVENT_CALL_MERGE_FAILED = "android.telecom.event.CALL_MERGE_FAILED";
+ field public static final java.lang.String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED";
field public static final java.lang.String EXTRA_CALL_SUBJECT = "android.telecom.extra.CALL_SUBJECT";
field public static final java.lang.String EXTRA_CHILD_ADDRESS = "android.telecom.extra.CHILD_ADDRESS";
field public static final java.lang.String EXTRA_LAST_FORWARDED_NUMBER = "android.telecom.extra.LAST_FORWARDED_NUMBER";
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 32; // 0x20
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 16; // 0x10
field public static final int STATE_ACTIVE = 4; // 0x4
field public static final int STATE_DIALING = 3; // 0x3
field public static final int STATE_DISCONNECTED = 6; // 0x6
field public static final int STATE_HOLDING = 5; // 0x5
field public static final int STATE_INITIALIZING = 0; // 0x0
field public static final int STATE_NEW = 1; // 0x1
+ field public static final int STATE_PULLING_CALL = 7; // 0x7
field public static final int STATE_RINGING = 2; // 0x2
}
@@ -36261,7 +36404,9 @@
method public java.lang.String getReason();
method public int getTone();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int ANSWERED_ELSEWHERE = 11; // 0xb
field public static final int BUSY = 7; // 0x7
+ field public static final int CALL_PULLED = 12; // 0xc
field public static final int CANCELED = 4; // 0x4
field public static final int CONNECTION_MANAGER_NOT_SUPPORTED = 10; // 0xa
field public static final android.os.Parcelable.Creator<android.telecom.DisconnectCause> CREATOR;
@@ -36297,6 +36442,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onCallRemoved(android.telecom.Call);
method public void onCanAddCallChanged(boolean);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public void onSilenceRinger();
method public final void setAudioRoute(int);
method public final void setMuted(boolean);
@@ -36419,6 +36565,7 @@
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConference, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionAdded(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConference, int);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConference, int);
method public void onConnectionRemoved(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onDestroyed(android.telecom.RemoteConference);
method public void onDisconnected(android.telecom.RemoteConference, android.telecom.DisconnectCause);
@@ -36437,6 +36584,7 @@
method public android.telecom.RemoteConference getConference();
method public java.util.List<android.telecom.RemoteConnection> getConferenceableConnections();
method public int getConnectionCapabilities();
+ method public int getConnectionProperties();
method public android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public int getState();
@@ -36448,6 +36596,7 @@
method public boolean isVoipAudioMode();
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
method public void registerCallback(android.telecom.RemoteConnection.Callback);
method public void registerCallback(android.telecom.RemoteConnection.Callback, android.os.Handler);
method public void reject();
@@ -36464,6 +36613,8 @@
method public void onConferenceChanged(android.telecom.RemoteConnection, android.telecom.RemoteConference);
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConnection, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConnection, int);
+ method public void onConnectionEvent(android.telecom.RemoteConnection, java.lang.String, android.os.Bundle);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConnection, int);
method public void onDestroyed(android.telecom.RemoteConnection);
method public void onDisconnected(android.telecom.RemoteConnection, android.telecom.DisconnectCause);
method public void onExtrasChanged(android.telecom.RemoteConnection, android.os.Bundle);
@@ -36560,6 +36711,7 @@
field public static final java.lang.String EXTRA_START_CALL_WITH_VIDEO_STATE = "android.telecom.extra.START_CALL_WITH_VIDEO_STATE";
field public static final java.lang.String GATEWAY_ORIGINAL_ADDRESS = "android.telecom.extra.GATEWAY_ORIGINAL_ADDRESS";
field public static final java.lang.String GATEWAY_PROVIDER_PACKAGE = "android.telecom.extra.GATEWAY_PROVIDER_PACKAGE";
+ field public static final java.lang.String METADATA_INCLUDE_EXTERNAL_CALLS = "android.telecom.INCLUDE_EXTERNAL_CALLS";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_RINGING = "android.telecom.IN_CALL_SERVICE_RINGING";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI";
field public static final int PRESENTATION_ALLOWED = 1; // 0x1
@@ -37200,6 +37352,26 @@
method public void onSubscriptionsChanged();
}
+ public final class TelephonyHistogram implements android.os.Parcelable {
+ ctor public TelephonyHistogram(int, int, int);
+ ctor public TelephonyHistogram(android.telephony.TelephonyHistogram);
+ ctor public TelephonyHistogram(android.os.Parcel);
+ method public void addTimeTaken(int);
+ method public int describeContents();
+ method public int getAverageTime();
+ method public int getBucketCount();
+ method public int[] getBucketCounters();
+ method public int[] getBucketEndPoints();
+ method public int getCategory();
+ method public int getId();
+ method public int getMaxTime();
+ method public int getMinTime();
+ method public int getSampleCount();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telephony.TelephonyHistogram> CREATOR;
+ field public static final int TELEPHONY_CATEGORY_RIL = 1; // 0x1
+ }
+
public class TelephonyManager {
method public boolean canChangeDtmfToneLength();
method public android.telephony.TelephonyManager createForSubscriptionId(int);
@@ -37230,6 +37402,7 @@
method public java.lang.String getSimSerialNumber();
method public int getSimState();
method public java.lang.String getSubscriberId();
+ method public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
method public java.lang.String getVoiceMailAlphaTag();
method public java.lang.String getVoiceMailNumber();
method public int getVoiceNetworkType();
@@ -40095,7 +40268,10 @@
method public boolean equals(android.util.DisplayMetrics);
method public void setTo(android.util.DisplayMetrics);
method public void setToDefaults();
+ field public static final int DENSITY_260 = 260; // 0x104
field public static final int DENSITY_280 = 280; // 0x118
+ field public static final int DENSITY_300 = 300; // 0x12c
+ field public static final int DENSITY_340 = 340; // 0x154
field public static final int DENSITY_360 = 360; // 0x168
field public static final int DENSITY_400 = 400; // 0x190
field public static final int DENSITY_420 = 420; // 0x1a4
@@ -41304,6 +41480,10 @@
field public static final int KEYCODE_FOCUS = 80; // 0x50
field public static final int KEYCODE_FORWARD = 125; // 0x7d
field public static final int KEYCODE_FORWARD_DEL = 112; // 0x70
+ field public static final int KEYCODE_FP_NAV_DOWN = 281; // 0x119
+ field public static final int KEYCODE_FP_NAV_LEFT = 282; // 0x11a
+ field public static final int KEYCODE_FP_NAV_RIGHT = 283; // 0x11b
+ field public static final int KEYCODE_FP_NAV_UP = 280; // 0x118
field public static final int KEYCODE_FUNCTION = 119; // 0x77
field public static final int KEYCODE_G = 35; // 0x23
field public static final int KEYCODE_GRAVE = 68; // 0x44
@@ -44512,6 +44692,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -44621,6 +44802,7 @@
field public static final int IME_NULL = 0; // 0x0
field public int actionId;
field public java.lang.CharSequence actionLabel;
+ field public java.lang.String[] contentMimeTypes;
field public android.os.Bundle extras;
field public int fieldId;
field public java.lang.String fieldName;
@@ -44680,6 +44862,7 @@
method public abstract boolean clearMetaKeyStates(int);
method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public abstract boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
method public abstract boolean deleteSurroundingText(int, int);
@@ -44713,6 +44896,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -44737,6 +44921,17 @@
method public void setTarget(android.view.inputmethod.InputConnection);
}
+ public class InputContentInfo implements android.os.Parcelable {
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription);
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri);
+ method public int describeContents();
+ method public android.net.Uri getContentUri();
+ method public android.content.ClipDescription getDescription();
+ method public android.net.Uri getLinkUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR;
+ }
+
public abstract interface InputMethod {
method public abstract void attachToken(android.os.IBinder);
method public abstract void bindInput(android.view.inputmethod.InputBinding);
diff --git a/api/system-current.txt b/api/system-current.txt
index 8409ee2..a49f790 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -527,7 +527,9 @@
field public static final int contentInsetStart = 16843859; // 0x1010453
field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522
field public static final int contextClickable = 16844007; // 0x10104e7
+ field public static final int contextDescription = 16844078; // 0x101052e
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844077; // 0x101052d
field public static final int controlX1 = 16843772; // 0x10103fc
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
@@ -1146,6 +1148,7 @@
field public static final int rotation = 16843558; // 0x1010326
field public static final int rotationX = 16843559; // 0x1010327
field public static final int rotationY = 16843560; // 0x1010328
+ field public static final int roundIcon = 16844076; // 0x101052c
field public static final int rowCount = 16843637; // 0x1010375
field public static final int rowDelay = 16843216; // 0x10101d0
field public static final int rowEdgeFlags = 16843329; // 0x1010241
@@ -1217,11 +1220,16 @@
field public static final int shareInterpolator = 16843195; // 0x10101bb
field public static final int sharedUserId = 16842763; // 0x101000b
field public static final int sharedUserLabel = 16843361; // 0x1010261
+ field public static final int shortcutDisabledMessage = 16844075; // 0x101052b
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutLongLabel = 16844074; // 0x101052a
+ field public static final int shortcutShortLabel = 16844073; // 0x1010529
field public static final int shouldDisableView = 16843246; // 0x10101ee
field public static final int showAsAction = 16843481; // 0x10102d9
field public static final int showDefault = 16843258; // 0x10101fa
field public static final int showDividers = 16843561; // 0x1010329
field public static final int showForAllUsers = 16844015; // 0x10104ef
+ field public static final int showMetadataInPreview = 16844079; // 0x101052f
field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9
field public static final int showSilent = 16843259; // 0x10101fb
field public static final int showText = 16843949; // 0x10104ad
@@ -5185,12 +5193,14 @@
method public android.app.Notification.Action.Builder extend(android.app.Notification.Action.Builder);
method public java.lang.CharSequence getCancelLabel();
method public java.lang.CharSequence getConfirmLabel();
+ method public boolean getHintDisplayActionInline();
method public boolean getHintLaunchesActivity();
method public java.lang.CharSequence getInProgressLabel();
method public boolean isAvailableOffline();
method public android.app.Notification.Action.WearableExtender setAvailableOffline(boolean);
method public android.app.Notification.Action.WearableExtender setCancelLabel(java.lang.CharSequence);
method public android.app.Notification.Action.WearableExtender setConfirmLabel(java.lang.CharSequence);
+ method public android.app.Notification.Action.WearableExtender setHintDisplayActionInline(boolean);
method public android.app.Notification.Action.WearableExtender setHintLaunchesActivity(boolean);
method public android.app.Notification.Action.WearableExtender setInProgressLabel(java.lang.CharSequence);
}
@@ -5904,7 +5914,10 @@
method public android.content.pm.ServiceInfo getServiceInfo();
method public java.lang.String getServiceName();
method public java.lang.String getSettingsActivity();
+ method public boolean getShowMetadataInPreview();
method public java.lang.CharSequence loadAuthor(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public java.lang.CharSequence loadContextDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public android.net.Uri loadContextUri(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence loadDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
method public java.lang.CharSequence loadLabel(android.content.pm.PackageManager);
@@ -6766,11 +6779,13 @@
method public android.content.res.Configuration getConfiguration();
method public int getEventType();
method public java.lang.String getPackageName();
+ method public java.lang.String getShortcutId();
method public long getTimeStamp();
field public static final int CONFIGURATION_CHANGE = 5; // 0x5
field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
field public static final int NONE = 0; // 0x0
+ field public static final int SHORTCUT_INVOCATION = 8; // 0x8
field public static final int USER_INTERACTION = 7; // 0x7
}
@@ -8514,6 +8529,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 SYSTEM_HEALTH_SERVICE = "systemhealth";
field public static final java.lang.String TELECOM_SERVICE = "telecom";
@@ -9854,13 +9870,22 @@
public class LauncherApps {
method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle);
+ method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(java.lang.String, 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 hasShortcutHostPermission();
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 boolean startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
}
@@ -9873,6 +9898,20 @@
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 android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
+ method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long);
+ method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String);
+ method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int);
+ method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List<java.lang.String>);
+ 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_MANIFEST = 8; // 0x8
+ field public static final int FLAG_GET_PINNED = 2; // 0x2
}
public class PackageInfo implements android.os.Parcelable {
@@ -10443,6 +10482,74 @@
field public java.lang.String permission;
}
+ public final class ShortcutInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.content.ComponentName getActivity();
+ method public java.util.Set<java.lang.String> getCategories();
+ method public java.lang.CharSequence getDisabledMessage();
+ method public int getDisabledMessageResourceId();
+ method public android.os.PersistableBundle getExtras();
+ method public int getIconResourceId();
+ method public java.lang.String getId();
+ method public android.content.Intent getIntent();
+ method public long getLastChangedTimestamp();
+ method public java.lang.CharSequence getLongLabel();
+ method public int getLongLabelResourceId();
+ method public java.lang.String getPackage();
+ method public int getRank();
+ method public java.lang.CharSequence getShortLabel();
+ method public int getShortLabelResourceId();
+ method public android.os.UserHandle getUserHandle();
+ method public boolean hasIconFile();
+ method public boolean hasIconResource();
+ method public boolean hasKeyFieldsOnly();
+ method public boolean hasStringResourcesResolved();
+ method public boolean isDynamic();
+ method public boolean isEnabled();
+ method public boolean isImmutable();
+ method public boolean isManifestShortcut();
+ method public boolean isPinned();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
+ field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
+ }
+
+ 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 setActivity(android.content.ComponentName);
+ method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set<java.lang.String>);
+ method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence);
+ 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 setLongLabel(java.lang.CharSequence);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence);
+ }
+
+ public class ShortcutManager {
+ method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
+ method public void disableShortcuts(java.util.List<java.lang.String>);
+ method public void disableShortcuts(java.util.List<java.lang.String>, int);
+ method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.String);
+ method public void enableShortcuts(java.util.List<java.lang.String>);
+ method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+ method public int getIconMaxHeight();
+ method public int getIconMaxWidth();
+ method public java.util.List<android.content.pm.ShortcutInfo> getManifestShortcuts();
+ method public int getMaxShortcutCountForActivity();
+ method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
+ method public long getRateLimitResetTime();
+ method public int getRemainingCallCount();
+ method public void removeAllDynamicShortcuts();
+ method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
+ method public void reportShortcutUsed(java.lang.String);
+ 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);
@@ -31543,6 +31650,7 @@
method public static void installPackage(android.content.Context, java.io.File, boolean) throws java.io.IOException;
method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener, android.os.Handler) throws java.io.IOException;
method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener) throws java.io.IOException;
+ method public static void rebootWipeAb(android.content.Context, java.io.File, java.lang.String) throws java.io.IOException;
method public static void rebootWipeCache(android.content.Context) throws java.io.IOException;
method public static void rebootWipeUserData(android.content.Context) throws java.io.IOException;
method public static void scheduleUpdateOnBoot(android.content.Context, java.io.File) throws java.io.IOException;
@@ -33212,6 +33320,7 @@
public static class CallLog.Calls implements android.provider.BaseColumns {
ctor public CallLog.Calls();
method public static java.lang.String getLastOutgoingCall(android.content.Context);
+ field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7
field public static final int BLOCKED_TYPE = 6; // 0x6
field public static final java.lang.String CACHED_FORMATTED_NUMBER = "formatted_number";
field public static final java.lang.String CACHED_LOOKUP_URI = "lookup_uri";
@@ -33234,6 +33343,7 @@
field public static final java.lang.String DURATION = "duration";
field public static final java.lang.String EXTRA_CALL_TYPE_FILTER = "android.provider.extra.CALL_TYPE_FILTER";
field public static final java.lang.String FEATURES = "features";
+ field public static final int FEATURES_PULLED_EXTERNALLY = 2; // 0x2
field public static final int FEATURES_VIDEO = 1; // 0x1
field public static final java.lang.String GEOCODED_LOCATION = "geocoded_location";
field public static final int INCOMING_TYPE = 1; // 0x1
@@ -34985,6 +35095,7 @@
field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
+ field public static final java.lang.String ACTION_DELETION_HELPER_SETTINGS = "android.settings.DELETION_HELPER_SETTINGS";
field public static final java.lang.String ACTION_DEVICE_INFO_SETTINGS = "android.settings.DEVICE_INFO_SETTINGS";
field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
@@ -35691,6 +35802,7 @@
field public static final int QUOTA_UNAVAILABLE = -1; // 0xffffffff
field public static final java.lang.String SETTINGS_URI = "settings_uri";
field public static final java.lang.String SOURCE_PACKAGE = "source_package";
+ field public static final java.lang.String SOURCE_TYPE = "source_type";
field public static final java.lang.String VOICEMAIL_ACCESS_URI = "voicemail_access_uri";
}
@@ -38771,10 +38883,15 @@
method public void phoneAccountSelected(android.telecom.PhoneAccountHandle, boolean);
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
+ method public final void putExtras(android.os.Bundle);
method public void registerCallback(android.telecom.Call.Callback);
method public void registerCallback(android.telecom.Call.Callback, android.os.Handler);
method public void reject(boolean, java.lang.String);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
method public deprecated void removeListener(android.telecom.Call.Listener);
+ method public void sendCallEvent(java.lang.String, android.os.Bundle);
method public void splitFromConference();
method public void stopDtmfTone();
method public void swapConference();
@@ -38789,6 +38906,7 @@
field public static final int STATE_HOLDING = 3; // 0x3
field public static final int STATE_NEW = 0; // 0x0
field public static final deprecated int STATE_PRE_DIAL_WAIT = 8; // 0x8
+ field public static final int STATE_PULLING_CALL = 11; // 0xb
field public static final int STATE_RINGING = 2; // 0x2
field public static final int STATE_SELECT_PHONE_ACCOUNT = 8; // 0x8
}
@@ -38799,6 +38917,7 @@
method public void onCannedTextResponsesLoaded(android.telecom.Call, java.util.List<java.lang.String>);
method public void onChildrenChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
method public void onConferenceableCallsChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public void onDetailsChanged(android.telecom.Call, android.telecom.Call.Details);
method public void onParentChanged(android.telecom.Call, android.telecom.Call);
method public void onPostDialWait(android.telecom.Call, java.lang.String);
@@ -38829,6 +38948,7 @@
method public static java.lang.String propertiesToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 8388608; // 0x800000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
field public static final int CAPABILITY_HOLD = 1; // 0x1
field public static final int CAPABILITY_MANAGE_CONFERENCE = 128; // 0x80
@@ -38848,7 +38968,9 @@
field public static final int PROPERTY_EMERGENCY_CALLBACK_MODE = 4; // 0x4
field public static final int PROPERTY_ENTERPRISE_CALL = 32; // 0x20
field public static final int PROPERTY_GENERIC_CONFERENCE = 2; // 0x2
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 128; // 0x80
field public static final int PROPERTY_HIGH_DEF_AUDIO = 16; // 0x10
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 64; // 0x40
field public static final int PROPERTY_WIFI = 8; // 0x8
}
@@ -38905,6 +39027,7 @@
method public final java.util.List<android.telecom.Connection> getConferenceableConnections();
method public final deprecated long getConnectTimeMillis();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final long getConnectionTime();
method public final java.util.List<android.telecom.Connection> getConnections();
method public final android.telecom.DisconnectCause getDisconnectCause();
@@ -38919,6 +39042,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onConnectionAdded(android.telecom.Connection);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onMerge(android.telecom.Connection);
method public void onMerge();
@@ -38927,15 +39051,19 @@
method public void onStopDtmfTone();
method public void onSwap();
method public void onUnhold();
+ method public final void putExtras(android.os.Bundle);
method public final void removeConnection(android.telecom.Connection);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
method public final void setActive();
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final deprecated void setConnectTimeMillis(long);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setConnectionTime(long);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setOnHold();
method public final void setStatusHints(android.telecom.StatusHints);
method public final void setVideoProvider(android.telecom.Connection, android.telecom.Connection.VideoProvider);
@@ -38962,6 +39090,7 @@
method public final android.telecom.Conference getConference();
method public final java.util.List<android.telecom.Conferenceable> getConferenceables();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public final int getState();
@@ -38973,16 +39102,24 @@
method public void onAnswer();
method public deprecated void onAudioStateChanged(android.telecom.AudioState);
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+ method public void onCallEvent(java.lang.String, android.os.Bundle);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onPlayDtmfTone(char);
method public void onPostDialContinue(boolean);
+ method public void onPullExternalCall();
method public void onReject();
method public void onReject(java.lang.String);
method public void onSeparate();
method public void onStateChanged(int);
method public void onStopDtmfTone();
method public void onUnhold();
+ method public static java.lang.String propertiesToString(int);
+ method public final void putExtras(android.os.Bundle);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
+ method public void sendConnectionEvent(java.lang.String, android.os.Bundle);
method public final void setActive();
method public final void setAddress(android.net.Uri, int);
method public final void setAudioModeIsVoip(boolean);
@@ -38990,9 +39127,10 @@
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final void setConferenceables(java.util.List<android.telecom.Conferenceable>);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setInitialized();
method public final void setInitializing();
method public final void setNextPostDialChar(char);
@@ -39006,6 +39144,7 @@
method public static java.lang.String stateToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 8388608; // 0x800000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 16777216; // 0x1000000
field public static final int CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 524288; // 0x80000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
@@ -39023,15 +39162,20 @@
field public static final int CAPABILITY_SUPPORTS_VT_REMOTE_TX = 2048; // 0x800
field public static final int CAPABILITY_SUPPORT_HOLD = 2; // 0x2
field public static final int CAPABILITY_SWAP_CONFERENCE = 8; // 0x8
+ field public static final java.lang.String EVENT_CALL_MERGE_FAILED = "android.telecom.event.CALL_MERGE_FAILED";
+ field public static final java.lang.String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED";
field public static final java.lang.String EXTRA_CALL_SUBJECT = "android.telecom.extra.CALL_SUBJECT";
field public static final java.lang.String EXTRA_CHILD_ADDRESS = "android.telecom.extra.CHILD_ADDRESS";
field public static final java.lang.String EXTRA_LAST_FORWARDED_NUMBER = "android.telecom.extra.LAST_FORWARDED_NUMBER";
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 32; // 0x20
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 16; // 0x10
field public static final int STATE_ACTIVE = 4; // 0x4
field public static final int STATE_DIALING = 3; // 0x3
field public static final int STATE_DISCONNECTED = 6; // 0x6
field public static final int STATE_HOLDING = 5; // 0x5
field public static final int STATE_INITIALIZING = 0; // 0x0
field public static final int STATE_NEW = 1; // 0x1
+ field public static final int STATE_PULLING_CALL = 7; // 0x7
field public static final int STATE_RINGING = 2; // 0x2
}
@@ -39109,7 +39253,9 @@
method public java.lang.String getReason();
method public int getTone();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int ANSWERED_ELSEWHERE = 11; // 0xb
field public static final int BUSY = 7; // 0x7
+ field public static final int CALL_PULLED = 12; // 0xc
field public static final int CANCELED = 4; // 0x4
field public static final int CONNECTION_MANAGER_NOT_SUPPORTED = 10; // 0xa
field public static final android.os.Parcelable.Creator<android.telecom.DisconnectCause> CREATOR;
@@ -39146,6 +39292,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onCallRemoved(android.telecom.Call);
method public void onCanAddCallChanged(boolean);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public deprecated void onPhoneCreated(android.telecom.Phone);
method public deprecated void onPhoneDestroyed(android.telecom.Phone);
method public void onSilenceRinger();
@@ -39183,14 +39330,16 @@
}
public class ParcelableCallAnalytics implements android.os.Parcelable {
- ctor public ParcelableCallAnalytics(long, long, int, boolean, boolean, int, int, boolean, java.lang.String, boolean);
+ ctor public ParcelableCallAnalytics(long, long, int, boolean, boolean, int, int, boolean, java.lang.String, boolean, java.util.List<android.telecom.ParcelableCallAnalytics.AnalyticsEvent>, java.util.List<android.telecom.ParcelableCallAnalytics.EventTiming>);
ctor public ParcelableCallAnalytics(android.os.Parcel);
+ method public java.util.List<android.telecom.ParcelableCallAnalytics.AnalyticsEvent> analyticsEvents();
method public int describeContents();
method public long getCallDurationMillis();
method public int getCallTechnologies();
method public int getCallTerminationCode();
method public int getCallType();
method public java.lang.String getConnectionService();
+ method public java.util.List<android.telecom.ParcelableCallAnalytics.EventTiming> getEventTimings();
method public long getStartTimeMillis();
method public boolean isAdditionalCall();
method public boolean isCreatedFromExistingConnection();
@@ -39211,6 +39360,73 @@
field public static final int THIRD_PARTY_PHONE = 16; // 0x10
}
+ public static final class ParcelableCallAnalytics.AnalyticsEvent implements android.os.Parcelable {
+ ctor public ParcelableCallAnalytics.AnalyticsEvent(int, long);
+ method public int describeContents();
+ method public int getEventName();
+ method public long getTimeSinceLastEvent();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final int AUDIO_ROUTE_BT = 204; // 0xcc
+ field public static final int AUDIO_ROUTE_EARPIECE = 205; // 0xcd
+ field public static final int AUDIO_ROUTE_HEADSET = 206; // 0xce
+ field public static final int AUDIO_ROUTE_SPEAKER = 207; // 0xcf
+ field public static final int BIND_CS = 5; // 0x5
+ field public static final int BLOCK_CHECK_FINISHED = 105; // 0x69
+ field public static final int BLOCK_CHECK_INITIATED = 104; // 0x68
+ field public static final int CONFERENCE_WITH = 300; // 0x12c
+ field public static final android.os.Parcelable.Creator<android.telecom.ParcelableCallAnalytics.AnalyticsEvent> CREATOR;
+ field public static final int CS_BOUND = 6; // 0x6
+ field public static final int DIRECT_TO_VM_FINISHED = 103; // 0x67
+ field public static final int DIRECT_TO_VM_INITIATED = 102; // 0x66
+ field public static final int FILTERING_COMPLETED = 107; // 0x6b
+ field public static final int FILTERING_INITIATED = 106; // 0x6a
+ field public static final int FILTERING_TIMED_OUT = 108; // 0x6c
+ field public static final int MUTE = 202; // 0xca
+ field public static final int REMOTELY_HELD = 402; // 0x192
+ field public static final int REMOTELY_UNHELD = 403; // 0x193
+ field public static final int REQUEST_ACCEPT = 7; // 0x7
+ field public static final int REQUEST_HOLD = 400; // 0x190
+ field public static final int REQUEST_PULL = 500; // 0x1f4
+ field public static final int REQUEST_REJECT = 8; // 0x8
+ field public static final int REQUEST_UNHOLD = 401; // 0x191
+ field public static final int SCREENING_COMPLETED = 101; // 0x65
+ field public static final int SCREENING_SENT = 100; // 0x64
+ field public static final int SET_ACTIVE = 1; // 0x1
+ field public static final int SET_DIALING = 4; // 0x4
+ field public static final int SET_DISCONNECTED = 2; // 0x2
+ field public static final int SET_HOLD = 404; // 0x194
+ field public static final int SET_PARENT = 302; // 0x12e
+ field public static final int SET_SELECT_PHONE_ACCOUNT = 0; // 0x0
+ field public static final int SILENCE = 201; // 0xc9
+ field public static final int SKIP_RINGING = 200; // 0xc8
+ field public static final int SPLIT_CONFERENCE = 301; // 0x12d
+ field public static final int START_CONNECTION = 3; // 0x3
+ field public static final int SWAP = 405; // 0x195
+ field public static final int UNMUTE = 203; // 0xcb
+ }
+
+ public static final class ParcelableCallAnalytics.EventTiming implements android.os.Parcelable {
+ ctor public ParcelableCallAnalytics.EventTiming(int, long);
+ method public int describeContents();
+ method public int getName();
+ method public long getTime();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final int ACCEPT_TIMING = 0; // 0x0
+ field public static final int BIND_CS_TIMING = 6; // 0x6
+ field public static final int BLOCK_CHECK_FINISHED_TIMING = 9; // 0x9
+ field public static final android.os.Parcelable.Creator<android.telecom.ParcelableCallAnalytics.EventTiming> CREATOR;
+ field public static final int DIRECT_TO_VM_FINISHED_TIMING = 8; // 0x8
+ field public static final int DISCONNECT_TIMING = 2; // 0x2
+ field public static final int FILTERING_COMPLETED_TIMING = 10; // 0xa
+ field public static final int FILTERING_TIMED_OUT_TIMING = 11; // 0xb
+ field public static final int HOLD_TIMING = 3; // 0x3
+ field public static final int INVALID = 999999; // 0xf423f
+ field public static final int OUTGOING_TIME_TO_DIALING_TIMING = 5; // 0x5
+ field public static final int REJECT_TIMING = 1; // 0x1
+ field public static final int SCREENING_COMPLETED_TIMING = 7; // 0x7
+ field public static final int UNHOLD_TIMING = 4; // 0x4
+ }
+
public final deprecated class Phone {
method public final void addListener(android.telecom.Phone.Listener);
method public final boolean canAddCall();
@@ -39323,6 +39539,7 @@
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConference, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionAdded(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConference, int);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConference, int);
method public void onConnectionRemoved(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onDestroyed(android.telecom.RemoteConference);
method public void onDisconnected(android.telecom.RemoteConference, android.telecom.DisconnectCause);
@@ -39341,6 +39558,7 @@
method public android.telecom.RemoteConference getConference();
method public java.util.List<android.telecom.RemoteConnection> getConferenceableConnections();
method public int getConnectionCapabilities();
+ method public int getConnectionProperties();
method public android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public int getState();
@@ -39352,6 +39570,7 @@
method public boolean isVoipAudioMode();
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
method public void registerCallback(android.telecom.RemoteConnection.Callback);
method public void registerCallback(android.telecom.RemoteConnection.Callback, android.os.Handler);
method public void reject();
@@ -39369,6 +39588,8 @@
method public void onConferenceChanged(android.telecom.RemoteConnection, android.telecom.RemoteConference);
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConnection, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConnection, int);
+ method public void onConnectionEvent(android.telecom.RemoteConnection, java.lang.String, android.os.Bundle);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConnection, int);
method public void onDestroyed(android.telecom.RemoteConnection);
method public void onDisconnected(android.telecom.RemoteConnection, android.telecom.DisconnectCause);
method public void onExtrasChanged(android.telecom.RemoteConnection, android.os.Bundle);
@@ -39422,6 +39643,41 @@
field public static final android.os.Parcelable.Creator<android.telecom.StatusHints> CREATOR;
}
+ public final class TelecomAnalytics implements android.os.Parcelable {
+ ctor public TelecomAnalytics(java.util.List<android.telecom.TelecomAnalytics.SessionTiming>, java.util.List<android.telecom.ParcelableCallAnalytics>);
+ method public int describeContents();
+ method public java.util.List<android.telecom.ParcelableCallAnalytics> getCallAnalytics();
+ method public java.util.List<android.telecom.TelecomAnalytics.SessionTiming> getSessionTimings();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telecom.TelecomAnalytics> CREATOR;
+ }
+
+ public static final class TelecomAnalytics.SessionTiming implements android.os.Parcelable {
+ ctor public TelecomAnalytics.SessionTiming(int, long);
+ method public int describeContents();
+ method public java.lang.Integer getKey();
+ method public long getTime();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telecom.TelecomAnalytics.SessionTiming> CREATOR;
+ field public static final int CSW_ADD_CONFERENCE_CALL = 108; // 0x6c
+ field public static final int CSW_HANDLE_CREATE_CONNECTION_COMPLETE = 100; // 0x64
+ field public static final int CSW_REMOVE_CALL = 106; // 0x6a
+ field public static final int CSW_SET_ACTIVE = 101; // 0x65
+ field public static final int CSW_SET_DIALING = 103; // 0x67
+ field public static final int CSW_SET_DISCONNECTED = 104; // 0x68
+ field public static final int CSW_SET_IS_CONFERENCED = 107; // 0x6b
+ field public static final int CSW_SET_ON_HOLD = 105; // 0x69
+ field public static final int CSW_SET_RINGING = 102; // 0x66
+ field public static final int ICA_ANSWER_CALL = 1; // 0x1
+ field public static final int ICA_CONFERENCE = 8; // 0x8
+ field public static final int ICA_DISCONNECT_CALL = 3; // 0x3
+ field public static final int ICA_HOLD_CALL = 4; // 0x4
+ field public static final int ICA_MUTE = 6; // 0x6
+ field public static final int ICA_REJECT_CALL = 2; // 0x2
+ field public static final int ICA_SET_AUDIO_ROUTE = 7; // 0x7
+ field public static final int ICA_UNHOLD_CALL = 5; // 0x5
+ }
+
public class TelecomManager {
method public void acceptRingingCall();
method public void acceptRingingCall(int);
@@ -39431,7 +39687,7 @@
method public deprecated void clearAccounts();
method public void clearPhoneAccounts();
method public android.content.Intent createManageBlockedNumbersIntent();
- method public java.util.List<android.telecom.ParcelableCallAnalytics> dumpAnalytics();
+ method public android.telecom.TelecomAnalytics dumpAnalytics();
method public void enablePhoneAccount(android.telecom.PhoneAccountHandle, boolean);
method public boolean endCall();
method public android.net.Uri getAdnUriForPhoneAccount(android.telecom.PhoneAccountHandle);
@@ -39492,6 +39748,7 @@
field public static final java.lang.String EXTRA_START_CALL_WITH_VIDEO_STATE = "android.telecom.extra.START_CALL_WITH_VIDEO_STATE";
field public static final java.lang.String GATEWAY_ORIGINAL_ADDRESS = "android.telecom.extra.GATEWAY_ORIGINAL_ADDRESS";
field public static final java.lang.String GATEWAY_PROVIDER_PACKAGE = "android.telecom.extra.GATEWAY_PROVIDER_PACKAGE";
+ field public static final java.lang.String METADATA_INCLUDE_EXTERNAL_CALLS = "android.telecom.INCLUDE_EXTERNAL_CALLS";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_RINGING = "android.telecom.IN_CALL_SERVICE_RINGING";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI";
field public static final int PRESENTATION_ALLOWED = 1; // 0x1
@@ -40134,6 +40391,26 @@
method public void onSubscriptionsChanged();
}
+ public final class TelephonyHistogram implements android.os.Parcelable {
+ ctor public TelephonyHistogram(int, int, int);
+ ctor public TelephonyHistogram(android.telephony.TelephonyHistogram);
+ ctor public TelephonyHistogram(android.os.Parcel);
+ method public void addTimeTaken(int);
+ method public int describeContents();
+ method public int getAverageTime();
+ method public int getBucketCount();
+ method public int[] getBucketCounters();
+ method public int[] getBucketEndPoints();
+ method public int getCategory();
+ method public int getId();
+ method public int getMaxTime();
+ method public int getMinTime();
+ method public int getSampleCount();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telephony.TelephonyHistogram> CREATOR;
+ field public static final int TELEPHONY_CATEGORY_RIL = 1; // 0x1
+ }
+
public class TelephonyManager {
method public void answerRingingCall();
method public void call(java.lang.String, java.lang.String);
@@ -40183,6 +40460,7 @@
method public java.lang.String getSimSerialNumber();
method public int getSimState();
method public java.lang.String getSubscriberId();
+ method public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
method public java.lang.String getVoiceMailAlphaTag();
method public java.lang.String getVoiceMailNumber();
method public int getVoiceNetworkType();
@@ -43094,7 +43372,10 @@
method public boolean equals(android.util.DisplayMetrics);
method public void setTo(android.util.DisplayMetrics);
method public void setToDefaults();
+ field public static final int DENSITY_260 = 260; // 0x104
field public static final int DENSITY_280 = 280; // 0x118
+ field public static final int DENSITY_300 = 300; // 0x12c
+ field public static final int DENSITY_340 = 340; // 0x154
field public static final int DENSITY_360 = 360; // 0x168
field public static final int DENSITY_400 = 400; // 0x190
field public static final int DENSITY_420 = 420; // 0x1a4
@@ -44303,6 +44584,10 @@
field public static final int KEYCODE_FOCUS = 80; // 0x50
field public static final int KEYCODE_FORWARD = 125; // 0x7d
field public static final int KEYCODE_FORWARD_DEL = 112; // 0x70
+ field public static final int KEYCODE_FP_NAV_DOWN = 281; // 0x119
+ field public static final int KEYCODE_FP_NAV_LEFT = 282; // 0x11a
+ field public static final int KEYCODE_FP_NAV_RIGHT = 283; // 0x11b
+ field public static final int KEYCODE_FP_NAV_UP = 280; // 0x118
field public static final int KEYCODE_FUNCTION = 119; // 0x77
field public static final int KEYCODE_G = 35; // 0x23
field public static final int KEYCODE_GRAVE = 68; // 0x44
@@ -47514,6 +47799,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -47623,6 +47909,7 @@
field public static final int IME_NULL = 0; // 0x0
field public int actionId;
field public java.lang.CharSequence actionLabel;
+ field public java.lang.String[] contentMimeTypes;
field public android.os.Bundle extras;
field public int fieldId;
field public java.lang.String fieldName;
@@ -47682,6 +47969,7 @@
method public abstract boolean clearMetaKeyStates(int);
method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public abstract boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
method public abstract boolean deleteSurroundingText(int, int);
@@ -47715,6 +48003,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -47739,6 +48028,17 @@
method public void setTarget(android.view.inputmethod.InputConnection);
}
+ public class InputContentInfo implements android.os.Parcelable {
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription);
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri);
+ method public int describeContents();
+ method public android.net.Uri getContentUri();
+ method public android.content.ClipDescription getDescription();
+ method public android.net.Uri getLinkUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR;
+ }
+
public abstract interface InputMethod {
method public abstract void attachToken(android.os.IBinder);
method public abstract void bindInput(android.view.inputmethod.InputBinding);
diff --git a/api/test-current.txt b/api/test-current.txt
index 57d6f66..0eb6804 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -420,7 +420,9 @@
field public static final int contentInsetStart = 16843859; // 0x1010453
field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522
field public static final int contextClickable = 16844007; // 0x10104e7
+ field public static final int contextDescription = 16844078; // 0x101052e
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844077; // 0x101052d
field public static final int controlX1 = 16843772; // 0x10103fc
field public static final int controlX2 = 16843774; // 0x10103fe
field public static final int controlY1 = 16843773; // 0x10103fd
@@ -1039,6 +1041,7 @@
field public static final int rotation = 16843558; // 0x1010326
field public static final int rotationX = 16843559; // 0x1010327
field public static final int rotationY = 16843560; // 0x1010328
+ field public static final int roundIcon = 16844076; // 0x101052c
field public static final int rowCount = 16843637; // 0x1010375
field public static final int rowDelay = 16843216; // 0x10101d0
field public static final int rowEdgeFlags = 16843329; // 0x1010241
@@ -1106,11 +1109,16 @@
field public static final int shareInterpolator = 16843195; // 0x10101bb
field public static final int sharedUserId = 16842763; // 0x101000b
field public static final int sharedUserLabel = 16843361; // 0x1010261
+ field public static final int shortcutDisabledMessage = 16844075; // 0x101052b
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutLongLabel = 16844074; // 0x101052a
+ field public static final int shortcutShortLabel = 16844073; // 0x1010529
field public static final int shouldDisableView = 16843246; // 0x10101ee
field public static final int showAsAction = 16843481; // 0x10102d9
field public static final int showDefault = 16843258; // 0x10101fa
field public static final int showDividers = 16843561; // 0x1010329
field public static final int showForAllUsers = 16844015; // 0x10104ef
+ field public static final int showMetadataInPreview = 16844079; // 0x101052f
field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9
field public static final int showSilent = 16843259; // 0x10101fb
field public static final int showText = 16843949; // 0x10104ad
@@ -5038,12 +5046,14 @@
method public android.app.Notification.Action.Builder extend(android.app.Notification.Action.Builder);
method public java.lang.CharSequence getCancelLabel();
method public java.lang.CharSequence getConfirmLabel();
+ method public boolean getHintDisplayActionInline();
method public boolean getHintLaunchesActivity();
method public java.lang.CharSequence getInProgressLabel();
method public boolean isAvailableOffline();
method public android.app.Notification.Action.WearableExtender setAvailableOffline(boolean);
method public android.app.Notification.Action.WearableExtender setCancelLabel(java.lang.CharSequence);
method public android.app.Notification.Action.WearableExtender setConfirmLabel(java.lang.CharSequence);
+ method public android.app.Notification.Action.WearableExtender setHintDisplayActionInline(boolean);
method public android.app.Notification.Action.WearableExtender setHintLaunchesActivity(boolean);
method public android.app.Notification.Action.WearableExtender setInProgressLabel(java.lang.CharSequence);
}
@@ -5762,7 +5772,10 @@
method public android.content.pm.ServiceInfo getServiceInfo();
method public java.lang.String getServiceName();
method public java.lang.String getSettingsActivity();
+ method public boolean getShowMetadataInPreview();
method public java.lang.CharSequence loadAuthor(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public java.lang.CharSequence loadContextDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
+ method public android.net.Uri loadContextUri(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence loadDescription(android.content.pm.PackageManager) throws android.content.res.Resources.NotFoundException;
method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
method public java.lang.CharSequence loadLabel(android.content.pm.PackageManager);
@@ -6490,11 +6503,13 @@
method public android.content.res.Configuration getConfiguration();
method public int getEventType();
method public java.lang.String getPackageName();
+ method public java.lang.String getShortcutId();
method public long getTimeStamp();
field public static final int CONFIGURATION_CHANGE = 5; // 0x5
field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
field public static final int NONE = 0; // 0x0
+ field public static final int SHORTCUT_INVOCATION = 8; // 0x8
field public static final int USER_INTERACTION = 7; // 0x7
}
@@ -8199,6 +8214,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 SYSTEM_HEALTH_SERVICE = "systemhealth";
field public static final java.lang.String TELECOM_SERVICE = "telecom";
@@ -9512,13 +9528,22 @@
public class LauncherApps {
ctor public LauncherApps(android.content.Context);
method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, android.os.UserHandle);
+ method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo);
+ method public android.os.ParcelFileDescriptor getShortcutIconFd(java.lang.String, 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 hasShortcutHostPermission();
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 boolean startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
}
@@ -9531,6 +9556,20 @@
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 android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
+ method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long);
+ method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String);
+ method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int);
+ method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List<java.lang.String>);
+ 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_MANIFEST = 8; // 0x8
+ field public static final int FLAG_GET_PINNED = 2; // 0x2
}
public class PackageInfo implements android.os.Parcelable {
@@ -10032,6 +10071,75 @@
field public java.lang.String permission;
}
+ public final class ShortcutInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.content.ComponentName getActivity();
+ method public java.util.Set<java.lang.String> getCategories();
+ method public java.lang.CharSequence getDisabledMessage();
+ method public int getDisabledMessageResourceId();
+ method public android.os.PersistableBundle getExtras();
+ method public int getIconResourceId();
+ method public java.lang.String getId();
+ method public android.content.Intent getIntent();
+ method public long getLastChangedTimestamp();
+ method public java.lang.CharSequence getLongLabel();
+ method public int getLongLabelResourceId();
+ method public java.lang.String getPackage();
+ method public int getRank();
+ method public java.lang.CharSequence getShortLabel();
+ method public int getShortLabelResourceId();
+ method public android.os.UserHandle getUserHandle();
+ method public boolean hasIconFile();
+ method public boolean hasIconResource();
+ method public boolean hasKeyFieldsOnly();
+ method public boolean hasStringResourcesResolved();
+ method public boolean isDynamic();
+ method public boolean isEnabled();
+ method public boolean isImmutable();
+ method public boolean isManifestShortcut();
+ method public boolean isPinned();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
+ field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
+ }
+
+ 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 setActivity(android.content.ComponentName);
+ method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set<java.lang.String>);
+ method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence);
+ 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 setLongLabel(java.lang.CharSequence);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence);
+ }
+
+ public class ShortcutManager {
+ ctor public ShortcutManager(android.content.Context);
+ method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
+ method public void disableShortcuts(java.util.List<java.lang.String>);
+ method public void disableShortcuts(java.util.List<java.lang.String>, int);
+ method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.String);
+ method public void enableShortcuts(java.util.List<java.lang.String>);
+ method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+ method public int getIconMaxHeight();
+ method public int getIconMaxWidth();
+ method public java.util.List<android.content.pm.ShortcutInfo> getManifestShortcuts();
+ method public int getMaxShortcutCountForActivity();
+ method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
+ method public long getRateLimitResetTime();
+ method public int getRemainingCallCount();
+ method public void removeAllDynamicShortcuts();
+ method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
+ method public void reportShortcutUsed(java.lang.String);
+ 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);
@@ -30713,6 +30821,7 @@
public static class CallLog.Calls implements android.provider.BaseColumns {
ctor public CallLog.Calls();
method public static java.lang.String getLastOutgoingCall(android.content.Context);
+ field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7
field public static final int BLOCKED_TYPE = 6; // 0x6
field public static final java.lang.String CACHED_FORMATTED_NUMBER = "formatted_number";
field public static final java.lang.String CACHED_LOOKUP_URI = "lookup_uri";
@@ -30735,6 +30844,7 @@
field public static final java.lang.String DURATION = "duration";
field public static final java.lang.String EXTRA_CALL_TYPE_FILTER = "android.provider.extra.CALL_TYPE_FILTER";
field public static final java.lang.String FEATURES = "features";
+ field public static final int FEATURES_PULLED_EXTERNALLY = 2; // 0x2
field public static final int FEATURES_VIDEO = 1; // 0x1
field public static final java.lang.String GEOCODED_LOCATION = "geocoded_location";
field public static final int INCOMING_TYPE = 1; // 0x1
@@ -32354,6 +32464,7 @@
field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
+ field public static final java.lang.String ACTION_DELETION_HELPER_SETTINGS = "android.settings.DELETION_HELPER_SETTINGS";
field public static final java.lang.String ACTION_DEVICE_INFO_SETTINGS = "android.settings.DEVICE_INFO_SETTINGS";
field public static final java.lang.String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
field public static final java.lang.String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
@@ -33060,6 +33171,7 @@
field public static final int QUOTA_UNAVAILABLE = -1; // 0xffffffff
field public static final java.lang.String SETTINGS_URI = "settings_uri";
field public static final java.lang.String SOURCE_PACKAGE = "source_package";
+ field public static final java.lang.String SOURCE_TYPE = "source_type";
field public static final java.lang.String VOICEMAIL_ACCESS_URI = "voicemail_access_uri";
}
@@ -36013,9 +36125,14 @@
method public void phoneAccountSelected(android.telecom.PhoneAccountHandle, boolean);
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
+ method public final void putExtras(android.os.Bundle);
method public void registerCallback(android.telecom.Call.Callback);
method public void registerCallback(android.telecom.Call.Callback, android.os.Handler);
method public void reject(boolean, java.lang.String);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
+ method public void sendCallEvent(java.lang.String, android.os.Bundle);
method public void splitFromConference();
method public void stopDtmfTone();
method public void swapConference();
@@ -36029,6 +36146,7 @@
field public static final int STATE_DISCONNECTING = 10; // 0xa
field public static final int STATE_HOLDING = 3; // 0x3
field public static final int STATE_NEW = 0; // 0x0
+ field public static final int STATE_PULLING_CALL = 11; // 0xb
field public static final int STATE_RINGING = 2; // 0x2
field public static final int STATE_SELECT_PHONE_ACCOUNT = 8; // 0x8
}
@@ -36039,6 +36157,7 @@
method public void onCannedTextResponsesLoaded(android.telecom.Call, java.util.List<java.lang.String>);
method public void onChildrenChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
method public void onConferenceableCallsChanged(android.telecom.Call, java.util.List<android.telecom.Call>);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public void onDetailsChanged(android.telecom.Call, android.telecom.Call.Details);
method public void onParentChanged(android.telecom.Call, android.telecom.Call);
method public void onPostDialWait(android.telecom.Call, java.lang.String);
@@ -36069,6 +36188,7 @@
method public static java.lang.String propertiesToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 8388608; // 0x800000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
field public static final int CAPABILITY_HOLD = 1; // 0x1
field public static final int CAPABILITY_MANAGE_CONFERENCE = 128; // 0x80
@@ -36088,7 +36208,9 @@
field public static final int PROPERTY_EMERGENCY_CALLBACK_MODE = 4; // 0x4
field public static final int PROPERTY_ENTERPRISE_CALL = 32; // 0x20
field public static final int PROPERTY_GENERIC_CONFERENCE = 2; // 0x2
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 128; // 0x80
field public static final int PROPERTY_HIGH_DEF_AUDIO = 16; // 0x10
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 64; // 0x40
field public static final int PROPERTY_WIFI = 8; // 0x8
}
@@ -36139,6 +36261,7 @@
method public final android.telecom.CallAudioState getCallAudioState();
method public final java.util.List<android.telecom.Connection> getConferenceableConnections();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final long getConnectionTime();
method public final java.util.List<android.telecom.Connection> getConnections();
method public final android.telecom.DisconnectCause getDisconnectCause();
@@ -36151,6 +36274,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onConnectionAdded(android.telecom.Connection);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onMerge(android.telecom.Connection);
method public void onMerge();
@@ -36159,14 +36283,18 @@
method public void onStopDtmfTone();
method public void onSwap();
method public void onUnhold();
+ method public final void putExtras(android.os.Bundle);
method public final void removeConnection(android.telecom.Connection);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
method public final void setActive();
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setConnectionTime(long);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setOnHold();
method public final void setStatusHints(android.telecom.StatusHints);
method public final void setVideoProvider(android.telecom.Connection, android.telecom.Connection.VideoProvider);
@@ -36192,6 +36320,7 @@
method public final android.telecom.Conference getConference();
method public final java.util.List<android.telecom.Conferenceable> getConferenceables();
method public final int getConnectionCapabilities();
+ method public final int getConnectionProperties();
method public final android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public final int getState();
@@ -36202,16 +36331,24 @@
method public void onAnswer(int);
method public void onAnswer();
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+ method public void onCallEvent(java.lang.String, android.os.Bundle);
method public void onDisconnect();
+ method public void onExtrasChanged(android.os.Bundle);
method public void onHold();
method public void onPlayDtmfTone(char);
method public void onPostDialContinue(boolean);
+ method public void onPullExternalCall();
method public void onReject();
method public void onReject(java.lang.String);
method public void onSeparate();
method public void onStateChanged(int);
method public void onStopDtmfTone();
method public void onUnhold();
+ method public static java.lang.String propertiesToString(int);
+ method public final void putExtras(android.os.Bundle);
+ method public final void removeExtras(java.util.List<java.lang.String>);
+ method public final void removeExtras(java.lang.String...);
+ method public void sendConnectionEvent(java.lang.String, android.os.Bundle);
method public final void setActive();
method public final void setAddress(android.net.Uri, int);
method public final void setAudioModeIsVoip(boolean);
@@ -36219,9 +36356,10 @@
method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
method public final void setConferenceables(java.util.List<android.telecom.Conferenceable>);
method public final void setConnectionCapabilities(int);
+ method public final void setConnectionProperties(int);
method public final void setDialing();
method public final void setDisconnected(android.telecom.DisconnectCause);
- method public final void setExtras(android.os.Bundle);
+ method public final deprecated void setExtras(android.os.Bundle);
method public final void setInitialized();
method public final void setInitializing();
method public final void setNextPostDialChar(char);
@@ -36235,6 +36373,7 @@
method public static java.lang.String stateToString(int);
field public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 8388608; // 0x800000
field public static final int CAPABILITY_CAN_PAUSE_VIDEO = 1048576; // 0x100000
+ field public static final int CAPABILITY_CAN_PULL_CALL = 16777216; // 0x1000000
field public static final int CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION = 4194304; // 0x400000
field public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 524288; // 0x80000
field public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 8192; // 0x2000
@@ -36252,15 +36391,20 @@
field public static final int CAPABILITY_SUPPORTS_VT_REMOTE_TX = 2048; // 0x800
field public static final int CAPABILITY_SUPPORT_HOLD = 2; // 0x2
field public static final int CAPABILITY_SWAP_CONFERENCE = 8; // 0x8
+ field public static final java.lang.String EVENT_CALL_MERGE_FAILED = "android.telecom.event.CALL_MERGE_FAILED";
+ field public static final java.lang.String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED";
field public static final java.lang.String EXTRA_CALL_SUBJECT = "android.telecom.extra.CALL_SUBJECT";
field public static final java.lang.String EXTRA_CHILD_ADDRESS = "android.telecom.extra.CHILD_ADDRESS";
field public static final java.lang.String EXTRA_LAST_FORWARDED_NUMBER = "android.telecom.extra.LAST_FORWARDED_NUMBER";
+ field public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 32; // 0x20
+ field public static final int PROPERTY_IS_EXTERNAL_CALL = 16; // 0x10
field public static final int STATE_ACTIVE = 4; // 0x4
field public static final int STATE_DIALING = 3; // 0x3
field public static final int STATE_DISCONNECTED = 6; // 0x6
field public static final int STATE_HOLDING = 5; // 0x5
field public static final int STATE_INITIALIZING = 0; // 0x0
field public static final int STATE_NEW = 1; // 0x1
+ field public static final int STATE_PULLING_CALL = 7; // 0x7
field public static final int STATE_RINGING = 2; // 0x2
}
@@ -36338,7 +36482,9 @@
method public java.lang.String getReason();
method public int getTone();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int ANSWERED_ELSEWHERE = 11; // 0xb
field public static final int BUSY = 7; // 0x7
+ field public static final int CALL_PULLED = 12; // 0xc
field public static final int CANCELED = 4; // 0x4
field public static final int CONNECTION_MANAGER_NOT_SUPPORTED = 10; // 0xa
field public static final android.os.Parcelable.Creator<android.telecom.DisconnectCause> CREATOR;
@@ -36374,6 +36520,7 @@
method public void onCallAudioStateChanged(android.telecom.CallAudioState);
method public void onCallRemoved(android.telecom.Call);
method public void onCanAddCallChanged(boolean);
+ method public void onConnectionEvent(android.telecom.Call, java.lang.String, android.os.Bundle);
method public void onSilenceRinger();
method public final void setAudioRoute(int);
method public final void setMuted(boolean);
@@ -36496,6 +36643,7 @@
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConference, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionAdded(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConference, int);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConference, int);
method public void onConnectionRemoved(android.telecom.RemoteConference, android.telecom.RemoteConnection);
method public void onDestroyed(android.telecom.RemoteConference);
method public void onDisconnected(android.telecom.RemoteConference, android.telecom.DisconnectCause);
@@ -36514,6 +36662,7 @@
method public android.telecom.RemoteConference getConference();
method public java.util.List<android.telecom.RemoteConnection> getConferenceableConnections();
method public int getConnectionCapabilities();
+ method public int getConnectionProperties();
method public android.telecom.DisconnectCause getDisconnectCause();
method public final android.os.Bundle getExtras();
method public int getState();
@@ -36525,6 +36674,7 @@
method public boolean isVoipAudioMode();
method public void playDtmfTone(char);
method public void postDialContinue(boolean);
+ method public void pullExternalCall();
method public void registerCallback(android.telecom.RemoteConnection.Callback);
method public void registerCallback(android.telecom.RemoteConnection.Callback, android.os.Handler);
method public void reject();
@@ -36541,6 +36691,8 @@
method public void onConferenceChanged(android.telecom.RemoteConnection, android.telecom.RemoteConference);
method public void onConferenceableConnectionsChanged(android.telecom.RemoteConnection, java.util.List<android.telecom.RemoteConnection>);
method public void onConnectionCapabilitiesChanged(android.telecom.RemoteConnection, int);
+ method public void onConnectionEvent(android.telecom.RemoteConnection, java.lang.String, android.os.Bundle);
+ method public void onConnectionPropertiesChanged(android.telecom.RemoteConnection, int);
method public void onDestroyed(android.telecom.RemoteConnection);
method public void onDisconnected(android.telecom.RemoteConnection, android.telecom.DisconnectCause);
method public void onExtrasChanged(android.telecom.RemoteConnection, android.os.Bundle);
@@ -36637,6 +36789,7 @@
field public static final java.lang.String EXTRA_START_CALL_WITH_VIDEO_STATE = "android.telecom.extra.START_CALL_WITH_VIDEO_STATE";
field public static final java.lang.String GATEWAY_ORIGINAL_ADDRESS = "android.telecom.extra.GATEWAY_ORIGINAL_ADDRESS";
field public static final java.lang.String GATEWAY_PROVIDER_PACKAGE = "android.telecom.extra.GATEWAY_PROVIDER_PACKAGE";
+ field public static final java.lang.String METADATA_INCLUDE_EXTERNAL_CALLS = "android.telecom.INCLUDE_EXTERNAL_CALLS";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_RINGING = "android.telecom.IN_CALL_SERVICE_RINGING";
field public static final java.lang.String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI";
field public static final int PRESENTATION_ALLOWED = 1; // 0x1
@@ -37277,6 +37430,26 @@
method public void onSubscriptionsChanged();
}
+ public final class TelephonyHistogram implements android.os.Parcelable {
+ ctor public TelephonyHistogram(int, int, int);
+ ctor public TelephonyHistogram(android.telephony.TelephonyHistogram);
+ ctor public TelephonyHistogram(android.os.Parcel);
+ method public void addTimeTaken(int);
+ method public int describeContents();
+ method public int getAverageTime();
+ method public int getBucketCount();
+ method public int[] getBucketCounters();
+ method public int[] getBucketEndPoints();
+ method public int getCategory();
+ method public int getId();
+ method public int getMaxTime();
+ method public int getMinTime();
+ method public int getSampleCount();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.telephony.TelephonyHistogram> CREATOR;
+ field public static final int TELEPHONY_CATEGORY_RIL = 1; // 0x1
+ }
+
public class TelephonyManager {
method public boolean canChangeDtmfToneLength();
method public android.telephony.TelephonyManager createForSubscriptionId(int);
@@ -37307,6 +37480,7 @@
method public java.lang.String getSimSerialNumber();
method public int getSimState();
method public java.lang.String getSubscriberId();
+ method public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
method public java.lang.String getVoiceMailAlphaTag();
method public java.lang.String getVoiceMailNumber();
method public int getVoiceNetworkType();
@@ -40174,7 +40348,10 @@
method public boolean equals(android.util.DisplayMetrics);
method public void setTo(android.util.DisplayMetrics);
method public void setToDefaults();
+ field public static final int DENSITY_260 = 260; // 0x104
field public static final int DENSITY_280 = 280; // 0x118
+ field public static final int DENSITY_300 = 300; // 0x12c
+ field public static final int DENSITY_340 = 340; // 0x154
field public static final int DENSITY_360 = 360; // 0x168
field public static final int DENSITY_400 = 400; // 0x190
field public static final int DENSITY_420 = 420; // 0x1a4
@@ -41383,6 +41560,10 @@
field public static final int KEYCODE_FOCUS = 80; // 0x50
field public static final int KEYCODE_FORWARD = 125; // 0x7d
field public static final int KEYCODE_FORWARD_DEL = 112; // 0x70
+ field public static final int KEYCODE_FP_NAV_DOWN = 281; // 0x119
+ field public static final int KEYCODE_FP_NAV_LEFT = 282; // 0x11a
+ field public static final int KEYCODE_FP_NAV_RIGHT = 283; // 0x11b
+ field public static final int KEYCODE_FP_NAV_UP = 280; // 0x118
field public static final int KEYCODE_FUNCTION = 119; // 0x77
field public static final int KEYCODE_G = 35; // 0x23
field public static final int KEYCODE_GRAVE = 68; // 0x44
@@ -44591,6 +44772,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -44700,6 +44882,7 @@
field public static final int IME_NULL = 0; // 0x0
field public int actionId;
field public java.lang.CharSequence actionLabel;
+ field public java.lang.String[] contentMimeTypes;
field public android.os.Bundle extras;
field public int fieldId;
field public java.lang.String fieldName;
@@ -44759,6 +44942,7 @@
method public abstract boolean clearMetaKeyStates(int);
method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public abstract boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
method public abstract boolean deleteSurroundingText(int, int);
@@ -44792,6 +44976,7 @@
method public boolean clearMetaKeyStates(int);
method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
+ method public boolean commitContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
method public boolean deleteSurroundingText(int, int);
@@ -44816,6 +45001,17 @@
method public void setTarget(android.view.inputmethod.InputConnection);
}
+ public class InputContentInfo implements android.os.Parcelable {
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription);
+ ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri);
+ method public int describeContents();
+ method public android.net.Uri getContentUri();
+ method public android.content.ClipDescription getDescription();
+ method public android.net.Uri getLinkUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR;
+ }
+
public abstract interface InputMethod {
method public abstract void attachToken(android.os.IBinder);
method public abstract void bindInput(android.view.inputmethod.InputBinding);
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index c597ed2..e8fcd3b 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -18,6 +18,9 @@
#define LOG_TAG "BootAnimation"
#include <stdint.h>
+#include <sys/inotify.h>
+#include <sys/poll.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <math.h>
#include <fcntl.h>
@@ -57,23 +60,29 @@
#include "BootAnimation.h"
#include "AudioPlayer.h"
-#define OEM_BOOTANIMATION_FILE "/oem/media/bootanimation.zip"
-#define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip"
-#define SYSTEM_ENCRYPTED_BOOTANIMATION_FILE "/system/media/bootanimation-encrypted.zip"
-#define EXIT_PROP_NAME "service.bootanim.exit"
-
namespace android {
+static const char OEM_BOOTANIMATION_FILE[] = "/oem/media/bootanimation.zip";
+static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip";
+static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip";
+static const char SYSTEM_DATA_DIR_PATH[] = "/data/system";
+static const char SYSTEM_TIME_DIR_NAME[] = "time";
+static const char SYSTEM_TIME_DIR_PATH[] = "/data/system/time";
+static const char LAST_TIME_CHANGED_FILE_NAME[] = "last_time_change";
+static const char LAST_TIME_CHANGED_FILE_PATH[] = "/data/system/time/last_time_change";
+static const char ACCURATE_TIME_FLAG_FILE_NAME[] = "time_is_accurate";
+static const char ACCURATE_TIME_FLAG_FILE_PATH[] = "/data/system/time/time_is_accurate";
+static const char EXIT_PROP_NAME[] = "service.bootanim.exit";
static const int ANIM_ENTRY_NAME_MAX = 256;
// ---------------------------------------------------------------------------
-BootAnimation::BootAnimation() : Thread(false), mClockEnabled(true) {
+BootAnimation::BootAnimation() : Thread(false), mClockEnabled(true), mTimeIsAccurate(false),
+ mTimeCheckThread(NULL) {
mSession = new SurfaceComposerClient();
}
-BootAnimation::~BootAnimation() {
-}
+BootAnimation::~BootAnimation() {}
void BootAnimation::onFirstRef() {
status_t err = mSession->linkToComposerDeath(this);
@@ -638,11 +647,21 @@
bool BootAnimation::movie()
{
-
Animation* animation = loadAnimation(mZipFileName);
if (animation == NULL)
return false;
+ bool anyPartHasClock = false;
+ for (size_t i=0; i < animation->parts.size(); i++) {
+ if(animation->parts[i].clockPosY >= 0) {
+ anyPartHasClock = true;
+ break;
+ }
+ }
+ if (!anyPartHasClock) {
+ mClockEnabled = false;
+ }
+
// Blend required to draw time on top of animation frames.
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glShadeModel(GL_FLAT);
@@ -664,7 +683,18 @@
mClockEnabled = clockTextureInitialized;
}
+ if (mClockEnabled && !updateIsTimeAccurate()) {
+ mTimeCheckThread = new TimeCheckThread(this);
+ mTimeCheckThread->run("BootAnimation::TimeCheckThread", PRIORITY_NORMAL);
+ }
+
playAnimation(*animation);
+
+ if (mTimeCheckThread != NULL) {
+ mTimeCheckThread->requestExit();
+ mTimeCheckThread = NULL;
+ }
+
releaseAnimation(animation);
if (clockTextureInitialized) {
@@ -745,7 +775,7 @@
// which is equivalent to mHeight - (yc + animation.height)
glDrawTexiOES(xc, mHeight - (yc + animation.height),
0, animation.width, animation.height);
- if (mClockEnabled && part.clockPosY >= 0) {
+ if (mClockEnabled && mTimeIsAccurate && part.clockPosY >= 0) {
drawTime(mClock, part.clockPosY);
}
@@ -824,6 +854,132 @@
mLoadedFiles.remove(fn);
return animation;
}
+
+bool BootAnimation::updateIsTimeAccurate() {
+ static constexpr long long MAX_TIME_IN_PAST = 60000LL * 60LL * 24LL * 30LL; // 30 days
+ static constexpr long long MAX_TIME_IN_FUTURE = 60000LL * 90LL; // 90 minutes
+
+ if (mTimeIsAccurate) {
+ return true;
+ }
+
+ struct stat statResult;
+ if(stat(ACCURATE_TIME_FLAG_FILE_PATH, &statResult) == 0) {
+ mTimeIsAccurate = true;
+ return true;
+ }
+
+ FILE* file = fopen(LAST_TIME_CHANGED_FILE_PATH, "r");
+ if (file != NULL) {
+ long long lastChangedTime = 0;
+ fscanf(file, "%lld", &lastChangedTime);
+ fclose(file);
+ if (lastChangedTime > 0) {
+ struct timespec now;
+ clock_gettime(CLOCK_REALTIME, &now);
+ // Match the Java timestamp format
+ long long rtcNow = (now.tv_sec * 1000LL) + (now.tv_nsec / 1000000LL);
+ if (lastChangedTime > rtcNow - MAX_TIME_IN_PAST
+ && lastChangedTime < rtcNow + MAX_TIME_IN_FUTURE) {
+ mTimeIsAccurate = true;
+ }
+ }
+ }
+
+ return mTimeIsAccurate;
+}
+
+BootAnimation::TimeCheckThread::TimeCheckThread(BootAnimation* bootAnimation) : Thread(false),
+ mInotifyFd(-1), mSystemWd(-1), mTimeWd(-1), mBootAnimation(bootAnimation) {}
+
+BootAnimation::TimeCheckThread::~TimeCheckThread() {
+ // mInotifyFd may be -1 but that's ok since we're not at risk of attempting to close a valid FD.
+ close(mInotifyFd);
+}
+
+bool BootAnimation::TimeCheckThread::threadLoop() {
+ bool shouldLoop = doThreadLoop() && !mBootAnimation->mTimeIsAccurate
+ && mBootAnimation->mClockEnabled;
+ if (!shouldLoop) {
+ close(mInotifyFd);
+ mInotifyFd = -1;
+ }
+ return shouldLoop;
+}
+
+bool BootAnimation::TimeCheckThread::doThreadLoop() {
+ static constexpr int BUFF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1));
+
+ // Poll instead of doing a blocking read so the Thread can exit if requested.
+ struct pollfd pfd = { mInotifyFd, POLLIN, 0 };
+ ssize_t pollResult = poll(&pfd, 1, 1000);
+
+ if (pollResult == 0) {
+ return true;
+ } else if (pollResult < 0) {
+ ALOGE("Could not poll inotify events");
+ return false;
+ }
+
+ char buff[BUFF_LEN] __attribute__ ((aligned(__alignof__(struct inotify_event))));;
+ ssize_t length = read(mInotifyFd, buff, BUFF_LEN);
+ if (length == 0) {
+ return true;
+ } else if (length < 0) {
+ ALOGE("Could not read inotify events");
+ return false;
+ }
+
+ const struct inotify_event *event;
+ for (char* ptr = buff; ptr < buff + length; ptr += sizeof(struct inotify_event) + event->len) {
+ event = (const struct inotify_event *) ptr;
+ if (event->wd == mSystemWd && strcmp(SYSTEM_TIME_DIR_NAME, event->name) == 0) {
+ addTimeDirWatch();
+ } else if (event->wd == mTimeWd && (strcmp(LAST_TIME_CHANGED_FILE_NAME, event->name) == 0
+ || strcmp(ACCURATE_TIME_FLAG_FILE_NAME, event->name) == 0)) {
+ return !mBootAnimation->updateIsTimeAccurate();
+ }
+ }
+
+ return true;
+}
+
+void BootAnimation::TimeCheckThread::addTimeDirWatch() {
+ mTimeWd = inotify_add_watch(mInotifyFd, SYSTEM_TIME_DIR_PATH,
+ IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB);
+ if (mTimeWd > 0) {
+ // No need to watch for the time directory to be created if it already exists
+ inotify_rm_watch(mInotifyFd, mSystemWd);
+ mSystemWd = -1;
+ }
+}
+
+status_t BootAnimation::TimeCheckThread::readyToRun() {
+ mInotifyFd = inotify_init();
+ if (mInotifyFd < 0) {
+ ALOGE("Could not initialize inotify fd");
+ return NO_INIT;
+ }
+
+ mSystemWd = inotify_add_watch(mInotifyFd, SYSTEM_DATA_DIR_PATH, IN_CREATE | IN_ATTRIB);
+ if (mSystemWd < 0) {
+ close(mInotifyFd);
+ mInotifyFd = -1;
+ ALOGE("Could not add watch for %s", SYSTEM_DATA_DIR_PATH);
+ return NO_INIT;
+ }
+
+ addTimeDirWatch();
+
+ if (mBootAnimation->updateIsTimeAccurate()) {
+ close(mInotifyFd);
+ mInotifyFd = -1;
+ return ALREADY_EXISTS;
+ }
+
+ return NO_ERROR;
+}
+
// ---------------------------------------------------------------------------
}
diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h
index d49e1ec..1c3d53a 100644
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -51,6 +51,24 @@
virtual void onFirstRef();
virtual void binderDied(const wp<IBinder>& who);
+ bool updateIsTimeAccurate();
+
+ class TimeCheckThread : public Thread {
+ public:
+ TimeCheckThread(BootAnimation* bootAnimation);
+ virtual ~TimeCheckThread();
+ private:
+ virtual status_t readyToRun();
+ virtual bool threadLoop();
+ bool doThreadLoop();
+ void addTimeDirWatch();
+
+ int mInotifyFd;
+ int mSystemWd;
+ int mTimeWd;
+ BootAnimation* mBootAnimation;
+ };
+
struct Texture {
GLint w;
GLint h;
@@ -113,8 +131,10 @@
sp<SurfaceControl> mFlingerSurfaceControl;
sp<Surface> mFlingerSurface;
bool mClockEnabled;
+ bool mTimeIsAccurate;
String8 mZipFileName;
SortedVector<String8> mLoadedFiles;
+ sp<TimeCheckThread> mTimeCheckThread;
};
// ---------------------------------------------------------------------------
diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java
index 224823e..ba16e67 100644
--- a/core/java/android/animation/PropertyValuesHolder.java
+++ b/core/java/android/animation/PropertyValuesHolder.java
@@ -1095,8 +1095,12 @@
}
// TODO: We need a better way to get data out of keyframes.
if (mKeyframes instanceof PathKeyframes.FloatKeyframesBase
- || mKeyframes instanceof PathKeyframes.IntKeyframesBase) {
- // property values will animate based on external data source (e.g. Path)
+ || mKeyframes instanceof PathKeyframes.IntKeyframesBase
+ || (mKeyframes.getKeyframes() != null && mKeyframes.getKeyframes().size() > 2)) {
+ // When a pvh has more than 2 keyframes, that means there are intermediate values in
+ // addition to start/end values defined for animators. Another case where such
+ // intermediate values are defined is when animator has a path to animate along. In
+ // these cases, a data source is needed to capture these intermediate values.
values.dataSource = new PropertyValues.DataSource() {
@Override
public Object getValueAtFraction(float fraction) {
@@ -1108,6 +1112,13 @@
}
}
+ /**
+ * @hide
+ */
+ public Class getValueType() {
+ return mValueType;
+ }
+
@Override
public String toString() {
return mPropertyName + ": " + mKeyframes.toString();
@@ -1178,6 +1189,15 @@
}
@Override
+ public void setProperty(Property property) {
+ if (property instanceof IntProperty) {
+ mIntProperty = (IntProperty) property;
+ } else {
+ super.setProperty(property);
+ }
+ }
+
+ @Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
@@ -1316,6 +1336,15 @@
}
@Override
+ public void setProperty(Property property) {
+ if (property instanceof FloatProperty) {
+ mFloatProperty = (FloatProperty) property;
+ } else {
+ super.setProperty(property);
+ }
+ }
+
+ @Override
public void setFloatValues(float... values) {
super.setFloatValues(values);
mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
@@ -1516,7 +1545,7 @@
}
propertyMap.put(mPropertyName, mJniSetter);
}
- }
+ }
}
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 13cf46d..ec8b288 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -4220,6 +4220,7 @@
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
@@ -4268,6 +4269,17 @@
}
}
+ private Bundle transferSpringboardActivityOptions(Bundle options) {
+ if (options == null && (mWindow != null && !mWindow.isActive())) {
+ final ActivityOptions activityOptions = getActivityOptions();
+ if (activityOptions != null &&
+ activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
+ return activityOptions.toBundle();
+ }
+ }
+ return options;
+ }
+
/**
* @hide Implement to provide correct calling token.
*/
@@ -4283,6 +4295,7 @@
if (mParent != null) {
throw new RuntimeException("Can't be called from a child");
}
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode,
options, user);
@@ -4318,6 +4331,7 @@
if (mParent != null) {
throw new RuntimeException("Can't be called from a child");
}
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
@@ -4350,6 +4364,7 @@
if (mParent != null) {
throw new RuntimeException("Can't be called from a child");
}
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivityAsCaller(
this, mMainThread.getApplicationThread(), mToken, this,
@@ -4789,6 +4804,7 @@
*/
public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
int requestCode, @Nullable Bundle options) {
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, child,
@@ -4854,6 +4870,7 @@
if (referrer != null) {
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
}
+ options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, who,
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 4c8ddc7..ccc37d7 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -31,10 +31,13 @@
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
+import android.transition.Transition;
+import android.transition.TransitionManager;
import android.util.Pair;
import android.util.Slog;
import android.view.AppTransitionAnimationSpec;
import android.view.View;
+import android.view.ViewGroup;
import android.view.Window;
import java.util.ArrayList;
@@ -640,10 +643,71 @@
public static ActivityOptions makeSceneTransitionAnimation(Activity activity,
Pair<View, String>... sharedElements) {
ActivityOptions opts = new ActivityOptions();
- if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
- opts.mAnimationType = ANIM_DEFAULT;
+ makeSceneTransitionAnimation(activity, activity.getWindow(), opts,
+ activity.mExitTransitionListener, sharedElements);
+ return opts;
+ }
+
+ /**
+ * Call this immediately prior to startActivity to begin a shared element transition
+ * from a non-Activity. The window must support Window.FEATURE_ACTIVITY_TRANSITIONS.
+ * The exit transition will start immediately and the shared element transition will
+ * start once the launched Activity's shared element is ready.
+ * <p>
+ * When all transitions have completed and the shared element has been transfered,
+ * the window's decor View will have its visibility set to View.GONE.
+ *
+ * @hide
+ */
+ @SafeVarargs
+ public static ActivityOptions startSharedElementAnimation(Window window,
+ Pair<View, String>... sharedElements) {
+ ActivityOptions opts = new ActivityOptions();
+ final View decorView = window.getDecorView();
+ if (decorView == null) {
return opts;
}
+ final ExitTransitionCoordinator exit =
+ makeSceneTransitionAnimation(null, window, opts, null, sharedElements);
+ if (exit != null) {
+ HideWindowListener listener = new HideWindowListener(window, exit);
+ exit.setHideSharedElementsCallback(listener);
+ exit.startExit();
+ }
+ return opts;
+ }
+
+ /**
+ * This method should be called when the {@link #startSharedElementAnimation(Window, Pair[])}
+ * animation must be stopped and the Views reset. This can happen if there was an error
+ * from startActivity or a springboard activity and the animation should stop and reset.
+ *
+ * @hide
+ */
+ public static void stopSharedElementAnimation(Window window) {
+ final View decorView = window.getDecorView();
+ if (decorView == null) {
+ return;
+ }
+ final ExitTransitionCoordinator exit = (ExitTransitionCoordinator)
+ decorView.getTag(com.android.internal.R.id.cross_task_transition);
+ if (exit != null) {
+ exit.cancelPendingTransitions();
+ decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, null);
+ TransitionManager.endTransitions((ViewGroup) decorView);
+ exit.resetViews();
+ exit.clearState();
+ decorView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ static ExitTransitionCoordinator makeSceneTransitionAnimation(Activity activity, Window window,
+ ActivityOptions opts, SharedElementCallback callback,
+ Pair<View, String>[] sharedElements) {
+ if (!window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
+ opts.mAnimationType = ANIM_DEFAULT;
+ return null;
+ }
opts.mAnimationType = ANIM_SCENE_TRANSITION;
ArrayList<String> names = new ArrayList<String>();
@@ -665,18 +729,22 @@
}
}
- ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, names, names,
- views, false);
+ ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, window,
+ callback, names, names, views, false);
opts.mTransitionReceiver = exit;
opts.mSharedElementNames = names;
- opts.mIsReturning = false;
- opts.mExitCoordinatorIndex =
- activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
- return opts;
+ opts.mIsReturning = (activity == null);
+ if (activity == null) {
+ opts.mExitCoordinatorIndex = -1;
+ } else {
+ opts.mExitCoordinatorIndex =
+ activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
+ }
+ return exit;
}
/** @hide */
- public static ActivityOptions makeSceneTransitionAnimation(Activity activity,
+ static ActivityOptions makeSceneTransitionAnimation(Activity activity,
ExitTransitionCoordinator exitCoordinator, ArrayList<String> sharedElementNames,
int resultCode, Intent resultData) {
ActivityOptions opts = new ActivityOptions();
@@ -900,6 +968,16 @@
return mIsReturning;
}
+ /**
+ * Returns whether or not the ActivityOptions was created with
+ * {@link #startSharedElementAnimation(Window, Pair[])}.
+ *
+ * @hide
+ */
+ boolean isCrossTask() {
+ return mExitCoordinatorIndex < 0;
+ }
+
/** @hide */
public ArrayList<String> getSharedElementNames() {
return mSharedElementNames;
@@ -1191,4 +1269,65 @@
+ ", mAnimationType=" + mAnimationType + ", mStartX=" + mStartX + ", mStartY="
+ mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight;
}
+
+ private static class HideWindowListener extends Transition.TransitionListenerAdapter
+ implements ExitTransitionCoordinator.HideSharedElementsCallback {
+ private final Window mWindow;
+ private final ExitTransitionCoordinator mExit;
+ private final boolean mWaitingForTransition;
+ private boolean mTransitionEnded;
+ private boolean mSharedElementHidden;
+ private ArrayList<View> mSharedElements;
+
+ public HideWindowListener(Window window, ExitTransitionCoordinator exit) {
+ mWindow = window;
+ mExit = exit;
+ mSharedElements = new ArrayList<>(exit.mSharedElements);
+ Transition transition = mWindow.getExitTransition();
+ if (transition != null) {
+ transition.addListener(this);
+ mWaitingForTransition = true;
+ } else {
+ mWaitingForTransition = false;
+ }
+ View decorView = mWindow.getDecorView();
+ if (decorView != null) {
+ if (decorView.getTag(com.android.internal.R.id.cross_task_transition) != null) {
+ throw new IllegalStateException(
+ "Cannot start a transition while one is running");
+ }
+ decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, exit);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ mTransitionEnded = true;
+ hideWhenDone();
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void hideSharedElements() {
+ mSharedElementHidden = true;
+ hideWhenDone();
+ }
+
+ private void hideWhenDone() {
+ if (mSharedElementHidden && (!mWaitingForTransition || mTransitionEnded)) {
+ mExit.resetViews();
+ int numSharedElements = mSharedElements.size();
+ for (int i = 0; i < numSharedElements; i++) {
+ View view = mSharedElements.get(i);
+ view.requestLayout();
+ }
+ View decorView = mWindow.getDecorView();
+ if (decorView != null) {
+ decorView.setTagInternal(
+ com.android.internal.R.id.cross_task_transition, null);
+ decorView.setVisibility(View.GONE);
+ }
+ }
+ }
+ }
}
diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java
index 02eb4d3..2219238 100644
--- a/core/java/android/app/ActivityTransitionState.java
+++ b/core/java/android/app/ActivityTransitionState.java
@@ -185,7 +185,12 @@
activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
}
mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
- resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning());
+ resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
+ mEnterActivityOptions.isCrossTask());
+ if (mEnterActivityOptions.isCrossTask()) {
+ mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
+ mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
+ }
if (!mIsEnterPostponed) {
startEnter();
@@ -275,7 +280,8 @@
}
private void restoreReenteringViews() {
- if (mEnterTransitionCoordinator != null && mEnterTransitionCoordinator.isReturning()) {
+ if (mEnterTransitionCoordinator != null && mEnterTransitionCoordinator.isReturning() &&
+ !mEnterTransitionCoordinator.isCrossTask()) {
mEnterTransitionCoordinator.forceViewsToAppear();
mExitingFrom = null;
mExitingTo = null;
@@ -302,8 +308,9 @@
}
}
- mReturnExitCoordinator =
- new ExitTransitionCoordinator(activity, mEnteringNames, null, null, true);
+ mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
+ activity.getWindow(), activity.mEnterTransitionListener, mEnteringNames,
+ null, null, true);
if (enterViewsTransition != null && decor != null) {
enterViewsTransition.resume(decor);
}
diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java
index 7824072..9928512 100644
--- a/core/java/android/app/AlertDialog.java
+++ b/core/java/android/app/AlertDialog.java
@@ -201,7 +201,7 @@
createContextThemeWrapper);
mWindow.alwaysReadCloseOnTouchAttr();
- mAlert = new AlertController(getContext(), this, getWindow());
+ mAlert = AlertController.create(getContext(), this, getWindow());
}
static int resolveDialogTheme(Context context, int themeResId) {
diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java
index 8bf1e9a..5d12b0d 100644
--- a/core/java/android/app/EnterTransitionCoordinator.java
+++ b/core/java/android/app/EnterTransitionCoordinator.java
@@ -59,12 +59,14 @@
private boolean mIsViewsTransitionStarted;
private Transition mEnterViewsTransition;
private OnPreDrawListener mViewsReadyListener;
+ private final boolean mIsCrossTask;
public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
- ArrayList<String> sharedElementNames, boolean isReturning) {
+ ArrayList<String> sharedElementNames, boolean isReturning, boolean isCrossTask) {
super(activity.getWindow(), sharedElementNames,
- getListener(activity, isReturning), isReturning);
+ getListener(activity, isReturning && !isCrossTask), isReturning);
mActivity = activity;
+ mIsCrossTask = isCrossTask;
setResultReceiver(resultReceiver);
prepareEnter();
Bundle resultReceiverBundle = new Bundle();
@@ -85,6 +87,10 @@
}
}
+ boolean isCrossTask() {
+ return mIsCrossTask;
+ }
+
public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
ArrayList<View> localViews) {
boolean remap = false;
@@ -325,7 +331,9 @@
if (mActivity == null || decorView == null) {
return;
}
- mActivity.overridePendingTransition(0, 0);
+ if (!isCrossTask()) {
+ mActivity.overridePendingTransition(0, 0);
+ }
if (!mIsReturning) {
mWasOpaque = mActivity.convertToTranslucent(null, null);
Drawable background = decorView.getBackground();
diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java
index 0404288..160c285 100644
--- a/core/java/android/app/ExitTransitionCoordinator.java
+++ b/core/java/android/app/ExitTransitionCoordinator.java
@@ -35,6 +35,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.view.Window;
import java.util.ArrayList;
@@ -59,18 +60,20 @@
private Bundle mExitSharedElementBundle;
private boolean mIsExitStarted;
private boolean mSharedElementsHidden;
+ private HideSharedElementsCallback mHideSharedElementsCallback;
- public ExitTransitionCoordinator(Activity activity, ArrayList<String> names,
+ public ExitTransitionCoordinator(Activity activity, Window window,
+ SharedElementCallback listener, ArrayList<String> names,
ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
- super(activity.getWindow(), names, getListener(activity, isReturning), isReturning);
+ super(window, names, listener, isReturning);
viewsReady(mapSharedElements(accepted, mapped));
stripOffscreenViews();
mIsBackgroundReady = !isReturning;
mActivity = activity;
}
- private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
- return isReturning ? activity.mEnterTransitionListener : activity.mExitTransitionListener;
+ void setHideSharedElementsCallback(HideSharedElementsCallback callback) {
+ mHideSharedElementsCallback = callback;
}
@Override
@@ -188,6 +191,9 @@
private void hideSharedElements() {
moveSharedElementsFromOverlay();
+ if (mHideSharedElementsCallback != null) {
+ mHideSharedElementsCallback.hideSharedElements();
+ }
if (!mIsHidden) {
hideViews(mSharedElements);
}
@@ -207,7 +213,11 @@
startTransition(new Runnable() {
@Override
public void run() {
- beginTransitions();
+ if (mActivity != null) {
+ beginTransitions();
+ } else {
+ startExitTransition();
+ }
}
});
}
@@ -508,4 +518,8 @@
return getWindow().getSharedElementExitTransition();
}
}
+
+ interface HideSharedElementsCallback {
+ void hideSharedElements();
+ }
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 6fc1820..eaea989 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1294,6 +1294,7 @@
// Flags bitwise-ored to mFlags
private static final int FLAG_AVAILABLE_OFFLINE = 0x1;
private static final int FLAG_HINT_LAUNCHES_ACTIVITY = 1 << 1;
+ private static final int FLAG_HINT_DISPLAY_INLINE = 1 << 2;
// Default value for flags integer
private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE;
@@ -1480,6 +1481,29 @@
public boolean getHintLaunchesActivity() {
return (mFlags & FLAG_HINT_LAUNCHES_ACTIVITY) != 0;
}
+
+ /**
+ * Set a hint that this Action should be displayed inline.
+ *
+ * @param hintDisplayInline {@code true} if action should be displayed inline, false
+ * otherwise
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintDisplayActionInline(
+ boolean hintDisplayInline) {
+ setFlag(FLAG_HINT_DISPLAY_INLINE, hintDisplayInline);
+ return this;
+ }
+
+ /**
+ * Get a hint that this Action should be displayed inline.
+ *
+ * @return {@code true} if the Action should be displayed inline, {@code false}
+ * otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintDisplayActionInline() {
+ return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
+ }
}
}
diff --git a/core/java/android/app/WallpaperInfo.java b/core/java/android/app/WallpaperInfo.java
index 7db9fa8..84a16cf 100644
--- a/core/java/android/app/WallpaperInfo.java
+++ b/core/java/android/app/WallpaperInfo.java
@@ -31,6 +31,7 @@
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.wallpaper.WallpaperService;
@@ -72,6 +73,10 @@
*/
final int mDescriptionResource;
+ final int mContextUriResource;
+ final int mContextDescriptionResource;
+ final boolean mShowMetadataInPreview;
+
/**
* Constructor.
*
@@ -89,7 +94,10 @@
int thumbnailRes = -1;
int authorRes = -1;
int descriptionRes = -1;
-
+ int contextUriRes = -1;
+ int contextDescriptionRes = -1;
+ boolean showMetadataInPreview = false;
+
XmlResourceParser parser = null;
try {
parser = si.loadXmlMetaData(pm, WallpaperService.SERVICE_META_DATA);
@@ -127,6 +135,15 @@
descriptionRes = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_description,
-1);
+ contextUriRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_contextUri,
+ -1);
+ contextDescriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_contextDescription,
+ -1);
+ showMetadataInPreview = sa.getBoolean(
+ com.android.internal.R.styleable.Wallpaper_showMetadataInPreview,
+ false);
sa.recycle();
} catch (NameNotFoundException e) {
@@ -140,6 +157,9 @@
mThumbnailResource = thumbnailRes;
mAuthorResource = authorRes;
mDescriptionResource = descriptionRes;
+ mContextUriResource = contextUriRes;
+ mContextDescriptionResource = contextDescriptionRes;
+ mShowMetadataInPreview = showMetadataInPreview;
}
WallpaperInfo(Parcel source) {
@@ -147,6 +167,9 @@
mThumbnailResource = source.readInt();
mAuthorResource = source.readInt();
mDescriptionResource = source.readInt();
+ mContextUriResource = source.readInt();
+ mContextDescriptionResource = source.readInt();
+ mShowMetadataInPreview = source.readInt() != 0;
mService = ResolveInfo.CREATOR.createFromParcel(source);
}
@@ -248,7 +271,55 @@
return pm.getText(packageName, mDescriptionResource,
mService.serviceInfo.applicationInfo);
}
-
+
+ /**
+ * Returns an URI that specifies a link for further context about this wallpaper.
+ *
+ * @param pm An instance of {@link PackageManager} to retrieve the URI.
+ * @return The URI.
+ */
+ public Uri loadContextUri(PackageManager pm) throws NotFoundException {
+ if (mContextUriResource <= 0) throw new NotFoundException();
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ String contextUriString = pm.getText(
+ packageName, mContextUriResource, applicationInfo).toString();
+ if (contextUriString == null) {
+ return null;
+ }
+ return Uri.parse(contextUriString);
+ }
+
+ /**
+ * Retrieves a title of the URI that specifies a link for further context about this wallpaper.
+ *
+ * @param pm An instance of {@link PackageManager} to retrieve the title.
+ * @return The title.
+ */
+ public CharSequence loadContextDescription(PackageManager pm) throws NotFoundException {
+ if (mContextDescriptionResource <= 0) throw new NotFoundException();
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ return pm.getText(packageName, mContextDescriptionResource, applicationInfo).toString();
+ }
+
+ /**
+ * Queries whether any metadata should be shown when previewing the wallpaper.
+ *
+ * @return Whether any metadata should be shown when previewing the wallpaper.
+ */
+ public boolean getShowMetadataInPreview() {
+ return mShowMetadataInPreview;
+ }
+
/**
* Return the class name of an activity that provides a settings UI for
* the wallpaper. You can launch this activity be starting it with
@@ -287,6 +358,9 @@
dest.writeInt(mThumbnailResource);
dest.writeInt(mAuthorResource);
dest.writeInt(mDescriptionResource);
+ dest.writeInt(mContextUriResource);
+ dest.writeInt(mContextDescriptionResource);
+ dest.writeInt(mShowMetadataInPreview ? 1 : 0);
mService.writeToParcel(dest, flags);
}
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 24647f38..a0da258 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -79,6 +79,13 @@
public static final int USER_INTERACTION = 7;
/**
+ * An event type denoting that an action equivalent to a ShortcutInfo is taken by the user.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public static final int SHORTCUT_INVOCATION = 8;
+
+ /**
* {@hide}
*/
public String mPackage;
@@ -105,6 +112,13 @@
public Configuration mConfiguration;
/**
+ * ID of the shortcut.
+ * Only present for {@link #SHORTCUT_INVOCATION} event types.
+ * {@hide}
+ */
+ public String mShortcutId;
+
+ /**
* The package name of the source of this event.
*/
public String getPackageName() {
@@ -145,6 +159,16 @@
public Configuration getConfiguration() {
return mConfiguration;
}
+
+ /**
+ * Returns the ID of a {@link android.content.pm.ShortcutInfo} for this event
+ * if the event is of type {@link #SHORTCUT_INVOCATION}, otherwise it returns null.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public String getShortcutId() {
+ return mShortcutId;
+ }
}
// Only used when creating the resulting events. Not used for reading/unparceling.
@@ -276,8 +300,13 @@
p.writeInt(event.mEventType);
p.writeLong(event.mTimeStamp);
- if (event.mEventType == Event.CONFIGURATION_CHANGE) {
- event.mConfiguration.writeToParcel(p, flags);
+ switch (event.mEventType) {
+ case Event.CONFIGURATION_CHANGE:
+ event.mConfiguration.writeToParcel(p, flags);
+ break;
+ case Event.SHORTCUT_INVOCATION:
+ p.writeString(event.mShortcutId);
+ break;
}
}
@@ -301,11 +330,18 @@
eventOut.mEventType = p.readInt();
eventOut.mTimeStamp = p.readLong();
- // Extract the configuration for configuration change events.
- if (eventOut.mEventType == Event.CONFIGURATION_CHANGE) {
- eventOut.mConfiguration = Configuration.CREATOR.createFromParcel(p);
- } else {
- eventOut.mConfiguration = null;
+ // Fill out the event-dependant fields.
+ eventOut.mConfiguration = null;
+ eventOut.mShortcutId = null;
+
+ switch (eventOut.mEventType) {
+ case Event.CONFIGURATION_CHANGE:
+ // Extract the configuration for configuration change events.
+ eventOut.mConfiguration = Configuration.CREATOR.createFromParcel(p);
+ break;
+ case Event.SHORTCUT_INVOCATION:
+ eventOut.mShortcutId = p.readString();
+ break;
}
}
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index b6f1567..a6f91fe 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -56,6 +56,17 @@
public abstract void reportConfigurationChange(Configuration config, int userId);
/**
+ * Reports that an action equivalent to a ShortcutInfo is taken by the user.
+ *
+ * @param packageName The package name of the shortcut publisher
+ * @param shortcutId The ID of the shortcut in question
+ * @param userId The user in which the content provider was accessed.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public abstract void reportShortcutUsage(String packageName, String shortcutId, int userId);
+
+ /**
* Reports that a content provider has been accessed by a foreground app.
* @param name The authority of the content provider
* @param pkgName The package name of the content provider
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index 2d9f4a7..cd14469 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -34,7 +34,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.util.TypedValue;
@@ -187,19 +186,28 @@
idsToUpdate[i] = mViews.keyAt(i);
}
}
- List<RemoteViews> updatedViews;
- int[] updatedIds = new int[idsToUpdate.length];
+ List<PendingHostUpdate> updates;
try {
- updatedViews = sService.startListening(
- mCallbacks, mContextOpPackageName, mHostId, idsToUpdate, updatedIds).getList();
+ updates = sService.startListening(
+ mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
- int N = updatedViews.size();
+ int N = updates.size();
for (int i = 0; i < N; i++) {
- updateAppWidgetView(updatedIds[i], updatedViews.get(i));
+ PendingHostUpdate update = updates.get(i);
+ switch (update.type) {
+ case PendingHostUpdate.TYPE_VIEWS_UPDATE:
+ updateAppWidgetView(update.appWidgetId, update.views);
+ break;
+ case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
+ onProviderChanged(update.appWidgetId, update.widgetInfo);
+ break;
+ case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
+ viewDataChanged(update.appWidgetId, update.viewId);
+ }
}
}
diff --git a/core/java/android/appwidget/PendingHostUpdate.java b/core/java/android/appwidget/PendingHostUpdate.java
new file mode 100644
index 0000000..5780319
--- /dev/null
+++ b/core/java/android/appwidget/PendingHostUpdate.java
@@ -0,0 +1,125 @@
+/*
+ * 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.appwidget;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.RemoteViews;
+
+/**
+ * @hide
+ */
+public class PendingHostUpdate implements Parcelable {
+
+ static final int TYPE_VIEWS_UPDATE = 0;
+ static final int TYPE_PROVIDER_CHANGED = 1;
+ static final int TYPE_VIEW_DATA_CHANGED = 2;
+
+ final int appWidgetId;
+ final int type;
+ RemoteViews views;
+ AppWidgetProviderInfo widgetInfo;
+ int viewId;
+
+ public static PendingHostUpdate updateAppWidget(int appWidgetId, RemoteViews views) {
+ PendingHostUpdate update = new PendingHostUpdate(appWidgetId, TYPE_VIEWS_UPDATE);
+ update.views = views;
+ return update;
+ }
+
+ public static PendingHostUpdate providerChanged(int appWidgetId, AppWidgetProviderInfo info) {
+ PendingHostUpdate update = new PendingHostUpdate(appWidgetId, TYPE_PROVIDER_CHANGED);
+ update.widgetInfo = info;
+ return update;
+ }
+
+ public static PendingHostUpdate viewDataChanged(int appWidgetId, int viewId) {
+ PendingHostUpdate update = new PendingHostUpdate(appWidgetId, TYPE_VIEW_DATA_CHANGED);
+ update.viewId = viewId;
+ return update;
+ }
+
+ private PendingHostUpdate(int appWidgetId, int type) {
+ this.appWidgetId = appWidgetId;
+ this.type = type;
+ }
+
+ private PendingHostUpdate(Parcel in) {
+ appWidgetId = in.readInt();
+ type = in.readInt();
+
+ switch (type) {
+ case TYPE_VIEWS_UPDATE:
+ if (0 != in.readInt()) {
+ views = new RemoteViews(in);
+ }
+ break;
+ case TYPE_PROVIDER_CHANGED:
+ if (0 != in.readInt()) {
+ widgetInfo = new AppWidgetProviderInfo(in);
+ }
+ break;
+ case TYPE_VIEW_DATA_CHANGED:
+ viewId = in.readInt();
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(appWidgetId);
+ dest.writeInt(type);
+ switch (type) {
+ case TYPE_VIEWS_UPDATE:
+ writeNullParcelable(views, dest, flags);
+ break;
+ case TYPE_PROVIDER_CHANGED:
+ writeNullParcelable(widgetInfo, dest, flags);
+ break;
+ case TYPE_VIEW_DATA_CHANGED:
+ dest.writeInt(viewId);
+ break;
+ }
+ }
+
+ private void writeNullParcelable(Parcelable p, Parcel dest, int flags) {
+ if (p != null) {
+ dest.writeInt(1);
+ p.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ /**
+ * Parcelable.Creator that instantiates PendingHostUpdate objects
+ */
+ public static final Parcelable.Creator<PendingHostUpdate> CREATOR
+ = new Parcelable.Creator<PendingHostUpdate>() {
+ public PendingHostUpdate createFromParcel(Parcel parcel) {
+ return new PendingHostUpdate(parcel);
+ }
+
+ public PendingHostUpdate[] newArray(int size) {
+ return new PendingHostUpdate[size];
+ }
+ };
+}
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
index 09a15de..f46a3b3 100644
--- a/core/java/android/bluetooth/BluetoothHeadset.java
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -220,6 +220,46 @@
* {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
* {@link #ACTION_AUDIO_STATE_CHANGED} intent.
*/
+
+ /**
+ * Intent used to broadcast the headset's indicator status
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_IND_ID} - The Assigned number of headset Indicator which is supported by
+ the headset ( as indicated by AT+BIND
+ command in the SLC sequence).or whose value
+ is changed (indicated by AT+BIEV command)</li>
+ * <li> {@link #EXTRA_IND_VALUE}- The updated value of headset indicator. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ * <p>{@link #EXTRA_IND_ID} is defined by Bluetooth SIG and each of the indicators are
+ * given an assigned number. Below shows the assigned number of Indicator added so far
+ * - Enhanced Safety - 1
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ * @hide
+ */
+ public static final String ACTION_HF_INDICATORS_VALUE_CHANGED =
+ "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED";
+
+ /**
+ * A String extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
+ * intents that contains the UUID of the headset indicator (as defined by Bluetooth SIG)
+ * that is being sent.
+ * @hide
+ */
+ public static final String EXTRA_HF_INDICATORS_IND_ID =
+ "android.bluetooth.headset.extra.HF_INDICATORS_IND_ID";
+
+ /**
+ * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
+ * intents that contains the value of the Headset indicator that is being sent.
+ * @hide
+ */
+ public static final String EXTRA_HF_INDICATORS_IND_VALUE =
+ "android.bluetooth.headset.extra.HF_INDICATORS_IND_VALUE";
+
public static final int STATE_AUDIO_CONNECTED = 12;
private static final int MESSAGE_HEADSET_SERVICE_CONNECTED = 100;
@@ -969,6 +1009,29 @@
return false;
}
+ /**
+ * Send Headset the BIND response from AG to report change in the status of the
+ * HF indicators to the headset
+ *
+ * @param ind_id Assigned Number of the indicator (defined by SIG)
+ * @param ind_status
+ * possible values- false-Indicator is disabled, no value changes shall be sent for this indicator
+ * true-Indicator is enabled, value changes may be sent for this indicator
+ * @hide
+ */
+ public void bindResponse(int ind_id, boolean ind_status) {
+ if (mService != null && isEnabled()) {
+ try {
+ mService.bindResponse(ind_id, ind_status);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ } else {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+
private final IBluetoothProfileServiceConnection mConnection
= new IBluetoothProfileServiceConnection.Stub() {
@Override
diff --git a/core/java/android/bluetooth/BluetoothSap.java b/core/java/android/bluetooth/BluetoothSap.java
index 014cb22..e70c936 100644
--- a/core/java/android/bluetooth/BluetoothSap.java
+++ b/core/java/android/bluetooth/BluetoothSap.java
@@ -138,7 +138,7 @@
}
boolean doBind() {
- Intent intent = new Intent(IBluetoothMap.class.getName());
+ Intent intent = new Intent(IBluetoothSap.class.getName());
ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
intent.setComponent(comp);
if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java
index ae12c88..ec01bef 100644
--- a/core/java/android/bluetooth/BluetoothSocket.java
+++ b/core/java/android/bluetooth/BluetoothSocket.java
@@ -532,22 +532,19 @@
if(length <= mMaxTxPacketSize) {
mSocketOS.write(b, offset, length);
} else {
- int tmpOffset = offset;
- int tmpLength = mMaxTxPacketSize;
- int endIndex = offset + length;
- boolean done = false;
if(DBG) Log.w(TAG, "WARNING: Write buffer larger than L2CAP packet size!\n"
+ "Packet will be divided into SDU packets of size "
+ mMaxTxPacketSize);
- do{
+ int tmpOffset = offset;
+ int bytesToWrite = length;
+ while (bytesToWrite > 0) {
+ int tmpLength = (bytesToWrite > mMaxTxPacketSize)
+ ? mMaxTxPacketSize
+ : bytesToWrite;
mSocketOS.write(b, tmpOffset, tmpLength);
- tmpOffset += mMaxTxPacketSize;
- if((tmpOffset + mMaxTxPacketSize) > endIndex) {
- tmpLength = endIndex - tmpOffset;
- done = true;
- }
- } while(!done);
-
+ tmpOffset += tmpLength;
+ bytesToWrite -= tmpLength;
+ }
}
} else {
mSocketOS.write(b, offset, length);
diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl
index 0bb4088..6ad442b 100755
--- a/core/java/android/bluetooth/IBluetoothHeadset.aidl
+++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl
@@ -59,4 +59,5 @@
String number, int type);
boolean enableWBS();
boolean disableWBS();
+ void bindResponse(int ind_id, boolean ind_status);
}
diff --git a/core/java/android/bluetooth/IBluetoothManager.aidl b/core/java/android/bluetooth/IBluetoothManager.aidl
index 0b81ee8..2b853a3 100644
--- a/core/java/android/bluetooth/IBluetoothManager.aidl
+++ b/core/java/android/bluetooth/IBluetoothManager.aidl
@@ -37,6 +37,7 @@
boolean enable();
boolean enableNoAutoConnect();
boolean disable(boolean persist);
+ int getState();
IBluetoothGatt getBluetoothGatt();
boolean bindBluetoothProfileService(int profile, IBluetoothProfileServiceConnection proxy);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 0a21939..4cfd76c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3619,8 +3619,6 @@
*
* @see #getSystemService
* @see android.content.pm.ShortcutManager
- *
- * @hide
*/
public static final String SHORTCUT_SERVICE = "shortcut";
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index ace54ba..c140f1b 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -3174,6 +3174,14 @@
public static final String ACTION_MASTER_CLEAR = "android.intent.action.MASTER_CLEAR";
/**
+ * Boolean intent extra to be used with {@link ACTION_MASTER_CLEAR} in order to force a factory
+ * reset even if {@link android.os.UserManager.DISALLOW_FACTORY_RESET} is set.
+ * @hide
+ */
+ public static final String EXTRA_FORCE_MASTER_CLEAR =
+ "android.intent.extra.FORCE_MASTER_CLEAR";
+
+ /**
* Broadcast action: report that a settings element is being restored from backup. The intent
* contains three extras: EXTRA_SETTING_NAME is a string naming the restored setting,
* EXTRA_SETTING_NEW_VALUE is the value being restored, and EXTRA_SETTING_PREVIOUS_VALUE
diff --git a/core/java/android/content/pm/EphemeralResolveInfo.java b/core/java/android/content/pm/EphemeralResolveInfo.java
index afb4c30..92d7945 100644
--- a/core/java/android/content/pm/EphemeralResolveInfo.java
+++ b/core/java/android/content/pm/EphemeralResolveInfo.java
@@ -27,6 +27,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
/**
* Information about an ephemeral application.
@@ -37,10 +38,7 @@
/** Algorithm that will be used to generate the domain digest */
public static final String SHA_ALGORITHM = "SHA-256";
- /** Full digest of the domain hash */
- private final byte[] mDigestBytes;
- /** The first 4 bytes of the domain hash */
- private final int mDigestPrefix;
+ private final EphemeralDigest mDigest;
private final String mPackageName;
/** The filters used to match domain */
private final List<IntentFilter> mFilters = new ArrayList<IntentFilter>();
@@ -55,29 +53,23 @@
throw new IllegalArgumentException();
}
- mDigestBytes = generateDigest(uri);
- mDigestPrefix =
- mDigestBytes[0] << 24
- | mDigestBytes[1] << 16
- | mDigestBytes[2] << 8
- | mDigestBytes[3] << 0;
+ mDigest = new EphemeralDigest(uri, -1);
mFilters.addAll(filters);
mPackageName = packageName;
}
EphemeralResolveInfo(Parcel in) {
- mDigestBytes = in.createByteArray();
- mDigestPrefix = in.readInt();
+ mDigest = in.readParcelable(null /*loader*/);
mPackageName = in.readString();
in.readList(mFilters, null /*loader*/);
}
public byte[] getDigestBytes() {
- return mDigestBytes;
+ return mDigest.getDigestBytes()[0];
}
public int getDigestPrefix() {
- return mDigestPrefix;
+ return mDigest.getDigestPrefix()[0];
}
public String getPackageName() {
@@ -88,16 +80,6 @@
return mFilters;
}
- private static byte[] generateDigest(Uri uri) {
- try {
- final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
- final byte[] hostBytes = uri.getHost().getBytes();
- return digest.digest(hostBytes);
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("could not find digest algorithm");
- }
- }
-
@Override
public int describeContents() {
return 0;
@@ -105,8 +87,7 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- out.writeByteArray(mDigestBytes);
- out.writeInt(mDigestPrefix);
+ out.writeParcelable(mDigest, flags);
out.writeString(mPackageName);
out.writeList(mFilters);
}
@@ -136,4 +117,120 @@
return mResolveInfo;
}
}
+
+ /**
+ * Helper class to generate and store each of the digests and prefixes
+ * sent to the Ephemeral Resolver.
+ * <p>
+ * Since intent filters may want to handle multiple hosts within a
+ * domain [eg “*.google.com”], the resolver is presented with multiple
+ * hash prefixes. For example, "a.b.c.d.e" generates digests for
+ * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e".
+ *
+ * @hide
+ */
+ public static final class EphemeralDigest implements Parcelable {
+ /** Full digest of the domain hashes */
+ private final byte[][] mDigestBytes;
+ /** The first 4 bytes of the domain hashes */
+ private final int[] mDigestPrefix;
+
+ public EphemeralDigest(@NonNull Uri uri, int maxDigests) {
+ if (uri == null) {
+ throw new IllegalArgumentException();
+ }
+ mDigestBytes = generateDigest(uri, maxDigests);
+ mDigestPrefix = new int[mDigestBytes.length];
+ for (int i = 0; i < mDigestBytes.length; i++) {
+ mDigestPrefix[i] =
+ (mDigestBytes[i][0] & 0xFF) << 24
+ | (mDigestBytes[i][1] & 0xFF) << 16
+ | (mDigestBytes[i][2] & 0xFF) << 8
+ | (mDigestBytes[i][3] & 0xFF) << 0;
+ }
+ }
+
+ private static byte[][] generateDigest(Uri uri, int maxDigests) {
+ ArrayList<byte[]> digests = new ArrayList<>();
+ try {
+ final String host = uri.getHost().toLowerCase(Locale.ENGLISH);
+ final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
+ if (maxDigests <= 0) {
+ final byte[] hostBytes = host.getBytes();
+ digests.add(digest.digest(hostBytes));
+ } else {
+ int prevDot = host.lastIndexOf('.');
+ prevDot = host.lastIndexOf('.', prevDot - 1);
+ // shortcut for short URLs
+ if (prevDot < 0) {
+ digests.add(digest.digest(host.getBytes()));
+ } else {
+ byte[] hostBytes = host.substring(prevDot + 1, host.length()).getBytes();
+ digests.add(digest.digest(hostBytes));
+ int digestCount = 1;
+ while (prevDot >= 0 && digestCount < maxDigests) {
+ prevDot = host.lastIndexOf('.', prevDot - 1);
+ hostBytes = host.substring(prevDot + 1, host.length()).getBytes();
+ digests.add(digest.digest(hostBytes));
+ digestCount++;
+ }
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("could not find digest algorithm");
+ }
+ return digests.toArray(new byte[digests.size()][]);
+ }
+
+ EphemeralDigest(Parcel in) {
+ final int digestCount = in.readInt();
+ if (digestCount == -1) {
+ mDigestBytes = null;
+ } else {
+ mDigestBytes = new byte[digestCount][];
+ for (int i = 0; i < digestCount; i++) {
+ mDigestBytes[i] = in.createByteArray();
+ }
+ }
+ mDigestPrefix = in.createIntArray();
+ }
+
+ public byte[][] getDigestBytes() {
+ return mDigestBytes;
+ }
+
+ public int[] getDigestPrefix() {
+ return mDigestPrefix;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (mDigestBytes == null) {
+ out.writeInt(-1);
+ } else {
+ out.writeInt(mDigestBytes.length);
+ for (int i = 0; i < mDigestBytes.length; i++) {
+ out.writeByteArray(mDigestBytes[i]);
+ }
+ }
+ out.writeIntArray(mDigestPrefix);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<EphemeralDigest> CREATOR =
+ new Parcelable.Creator<EphemeralDigest>() {
+ public EphemeralDigest createFromParcel(Parcel in) {
+ return new EphemeralDigest(in);
+ }
+
+ public EphemeralDigest[] newArray(int size) {
+ return new EphemeralDigest[size];
+ }
+ };
+ }
}
diff --git a/core/java/android/content/pm/IOtaDexopt.aidl b/core/java/android/content/pm/IOtaDexopt.aidl
index 8f38d6f..786a77f 100644
--- a/core/java/android/content/pm/IOtaDexopt.aidl
+++ b/core/java/android/content/pm/IOtaDexopt.aidl
@@ -42,6 +42,12 @@
boolean isDone();
/**
+ * Return the progress (0..1) made in this session. When {@link #isDone() isDone} returns
+ * true, the progress value will be 1.
+ */
+ float getProgress();
+
+ /**
* Optimize the next package. Note: this command is synchronous, that is, only returns after
* the package has been dexopted (or dexopting failed).
*/
diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index 2ba24f6..1c373f9 100644
--- a/core/java/android/content/pm/IShortcutService.aidl
+++ b/core/java/android/content/pm/IShortcutService.aidl
@@ -28,6 +28,8 @@
ParceledListSlice getDynamicShortcuts(String packageName, int userId);
+ ParceledListSlice getManifestShortcuts(String packageName, int userId);
+
boolean addDynamicShortcuts(String packageName, in ParceledListSlice shortcutInfoList,
int userId);
@@ -39,6 +41,11 @@
boolean updateShortcuts(String packageName, in ParceledListSlice shortcuts, int userId);
+ void disableShortcuts(String packageName, in List shortcutIds, String disabledMessage,
+ int disabledMessageResId, int userId);
+
+ void enableShortcuts(String packageName, in List shortcutIds, int userId);
+
int getMaxDynamicShortcutCount(String packageName, int userId);
int getRemainingCallCount(String packageName, int userId);
@@ -47,6 +54,8 @@
int getIconMaxDimensions(String packageName, int userId);
+ void reportShortcutUsed(String packageName, String shortcutId, int userId);
+
void resetThrottling(); // system only API for developer opsions
void onApplicationActive(String packageName, int userId); // system only API for sysUI
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 8ca27c5..a76bf24 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -24,7 +24,13 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -34,8 +40,10 @@
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.DisplayMetrics;
import android.util.Log;
+import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -163,11 +171,10 @@
* as defined in {@link #hasShortcutHostPermission()}, will receive it.
*
* @param packageName The name of the package that has the shortcuts.
- * @param shortcuts all shortcuts from the package (dynamic and/or pinned). Only "key"
- * information will be provided, as defined in {@link ShortcutInfo#hasKeyFieldsOnly()}.
+ * @param shortcuts all shortcuts from the package (dynamic, manifest and/or pinned).
+ * Only "key" information will be provided, as defined in
+ * {@link ShortcutInfo#hasKeyFieldsOnly()}.
* @param user The UserHandle of the profile that generated the change.
- *
- * @hide
*/
public void onShortcutsChanged(@NonNull String packageName,
@NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
@@ -176,8 +183,6 @@
/**
* Represents a query passed to {@link #getShortcuts(ShortcutQuery, UserHandle)}.
- *
- * @hide
*/
public static class ShortcutQuery {
/**
@@ -191,6 +196,15 @@
public static final int FLAG_GET_PINNED = 1 << 1;
/**
+ * Include manifest shortcuts in the result.
+ */
+ public static final int FLAG_GET_MANIFEST = 1 << 3;
+
+ /** @hide */
+ public static final int FLAG_GET_ALL_KINDS =
+ FLAG_GET_DYNAMIC | FLAG_GET_PINNED | FLAG_GET_MANIFEST;
+
+ /**
* Requests "key" fields only. See {@link ShortcutInfo#hasKeyFieldsOnly()} for which
* fields are available.
*/
@@ -201,6 +215,7 @@
value = {
FLAG_GET_DYNAMIC,
FLAG_GET_PINNED,
+ FLAG_GET_MANIFEST,
FLAG_GET_KEY_FIELDS_ONLY,
})
@Retention(RetentionPolicy.SOURCE)
@@ -227,37 +242,44 @@
* 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) {
+ public ShortcutQuery setChangedSince(long changedSince) {
mChangedSince = changedSince;
+ return this;
}
/**
* If non-null, returns only shortcuts from the package.
*/
- public void setPackage(@Nullable String packageName) {
+ public ShortcutQuery setPackage(@Nullable String packageName) {
mPackage = packageName;
+ return this;
}
/**
* If non-null, return only the specified shortcuts by ID. When setting this field,
* a packange name must also be set with {@link #setPackage}.
*/
- public void setShortcutIds(@Nullable List<String> shortcutIds) {
+ public ShortcutQuery setShortcutIds(@Nullable List<String> shortcutIds) {
mShortcutIds = shortcutIds;
+ return this;
}
/**
- * If non-null, returns only shortcuts associated with the activity.
+ * If non-null, returns only shortcuts associated with the activity; i.e.
+ * {@link ShortcutInfo}s whose {@link ShortcutInfo#getActivity()} are equal
+ * to {@code activity}.
*/
- public void setActivity(@Nullable ComponentName activity) {
+ public ShortcutQuery setActivity(@Nullable ComponentName activity) {
mActivity = activity;
+ return this;
}
/**
* Set query options.
*/
- public void setQueryFlags(@QueryFlags int queryFlags) {
+ public ShortcutQuery setQueryFlags(@QueryFlags int queryFlags) {
mQueryFlags = queryFlags;
+ return this;
}
}
@@ -426,8 +448,6 @@
* the user is trying a new launcher application. The user may decide to change the default
* launcher to the calling application again, so even if a launcher application loses
* this permission, it does <b>not</b> have to purge pinned shortcut information.
- *
- * @hide
*/
public boolean hasShortcutHostPermission() {
try {
@@ -447,8 +467,6 @@
* @param user The UserHandle of the profile.
*
* @return the IDs of {@link ShortcutInfo}s that match the query.
- *
- * @hide
*/
@Nullable
public List<ShortcutInfo> getShortcuts(@NonNull ShortcutQuery query,
@@ -472,7 +490,7 @@
final ShortcutQuery q = new ShortcutQuery();
q.setPackage(packageName);
q.setShortcutIds(ids);
- q.setQueryFlags(ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED);
+ q.setQueryFlags(ShortcutQuery.FLAG_GET_ALL_KINDS);
return getShortcuts(q, user);
}
@@ -488,8 +506,6 @@
* @param packageName The target package name.
* @param shortcutIds The IDs of the shortcut to be pinned.
* @param user The UserHandle of the profile.
- *
- * @hide
*/
public void pinShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds,
@NonNull UserHandle user) {
@@ -515,7 +531,7 @@
final ShortcutQuery q = new ShortcutQuery();
q.setPackage(packageName);
q.setShortcutIds(Arrays.asList(shortcutId));
- q.setQueryFlags(ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED);
+ q.setQueryFlags(ShortcutQuery.FLAG_GET_ALL_KINDS);
final List<ShortcutInfo> shortcuts = getShortcuts(q, user);
return shortcuts.size() > 0 ? shortcuts.get(0).getIconResourceId() : 0;
@@ -529,12 +545,10 @@
* #hasShortcutHostPermission()}.
*
* @param shortcut The target shortcut.
- *
- * @hide
*/
public ParcelFileDescriptor getShortcutIconFd(
@NonNull ShortcutInfo shortcut) {
- return getShortcutIconFd(shortcut.getPackageName(), shortcut.getId(),
+ return getShortcutIconFd(shortcut.getPackage(), shortcut.getId(),
shortcut.getUserId());
}
@@ -548,8 +562,6 @@
* @param packageName The target package name.
* @param shortcutId The ID of the shortcut to lad rom.
* @param user The UserHandle of the profile.
- *
- * @hide
*/
public ParcelFileDescriptor getShortcutIconFd(
@NonNull String packageName, @NonNull String shortcutId, @NonNull UserHandle user) {
@@ -567,6 +579,63 @@
}
/**
+ * Returns the icon for this shortcut, without any badging for the profile.
+ *
+ * @param density The preferred density of the icon, zero for default density. Use
+ * density DPI values from {@link DisplayMetrics}.
+ * @see #getShortcutBadgedIconDrawable(ShortcutInfo, int)
+ * @see DisplayMetrics
+ * @return The drawable associated with the shortcut.
+ */
+ public Drawable getShortcutIconDrawable(@NonNull ShortcutInfo shortcut, int density) {
+ if (shortcut.hasIconFile()) {
+ final ParcelFileDescriptor pfd = getShortcutIconFd(shortcut);
+ if (pfd == null) {
+ return null;
+ }
+ try {
+ final Bitmap bmp = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
+ return (bmp == null) ? null : new BitmapDrawable(mContext.getResources(), bmp);
+ } finally {
+ try {
+ pfd.close();
+ } catch (IOException ignore) {
+ }
+ }
+ } else if (shortcut.hasIconResource()) {
+ try {
+ final int resId = shortcut.getIconResourceId();
+ if (resId == 0) {
+ return null; // Shouldn't happen but just in case.
+ }
+ final ApplicationInfo ai = getApplicationInfo(shortcut.getPackage(),
+ /* flags =*/ 0, shortcut.getUserHandle());
+ final Resources res = mContext.getPackageManager().getResourcesForApplication(ai);
+ return res.getDrawableForDensity(resId, density);
+ } catch (NameNotFoundException | Resources.NotFoundException e) {
+ return null;
+ }
+ } else {
+ return null; // Has no icon.
+ }
+ }
+
+ /**
+ * Returns the shortcut icon with badging appropriate for the profile.
+ *
+ * @param density Optional density for the icon, or 0 to use the default density. Use
+ * {@link DisplayMetrics} for DPI values.
+ * @see DisplayMetrics
+ * @return A badged icon for the shortcut.
+ */
+ public Drawable getShortcutBadgedIconDrawable(ShortcutInfo shortcut, int density) {
+ final Drawable originalIcon = getShortcutIconDrawable(shortcut, density);
+
+ return (originalIcon == null) ? null : mContext.getPackageManager().getUserBadgedIcon(
+ originalIcon, shortcut.getUserHandle());
+ }
+
+ /**
* Launches a shortcut.
*
* <p>Callers must be allowed to access the shortcut information, as defined in {@link
@@ -579,8 +648,6 @@
* @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.
- *
- * @hide
*/
public boolean startShortcut(@NonNull String packageName, @NonNull String shortcutId,
@Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions,
@@ -600,12 +667,10 @@
* @param startActivityOptions Options to pass to startActivity.
* @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.
- *
- * @hide
*/
public boolean startShortcut(@NonNull ShortcutInfo shortcut,
@Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) {
- return startShortcut(shortcut.getPackageName(), shortcut.getId(),
+ return startShortcut(shortcut.getPackage(), shortcut.getId(),
sourceBounds, startActivityOptions,
shortcut.getUserId());
}
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index adf9fe6..f59a7dd 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -270,6 +270,7 @@
final int nameRes;
final int labelRes;
final int iconRes;
+ final int roundIconRes;
final int logoRes;
final int bannerRes;
@@ -277,7 +278,8 @@
TypedArray sa;
ParsePackageItemArgs(Package _owner, String[] _outError,
- int _nameRes, int _labelRes, int _iconRes, int _logoRes, int _bannerRes) {
+ int _nameRes, int _labelRes, int _iconRes, int _roundIconRes, int _logoRes,
+ int _bannerRes) {
owner = _owner;
outError = _outError;
nameRes = _nameRes;
@@ -285,6 +287,7 @@
iconRes = _iconRes;
logoRes = _logoRes;
bannerRes = _bannerRes;
+ roundIconRes = _roundIconRes;
}
}
@@ -296,10 +299,12 @@
int flags;
ParseComponentArgs(Package _owner, String[] _outError,
- int _nameRes, int _labelRes, int _iconRes, int _logoRes, int _bannerRes,
+ int _nameRes, int _labelRes, int _iconRes, int _roundIconRes, int _logoRes,
+ int _bannerRes,
String[] _sepProcesses, int _processRes,
int _descriptionRes, int _enabledRes) {
- super(_owner, _outError, _nameRes, _labelRes, _iconRes, _logoRes, _bannerRes);
+ super(_owner, _outError, _nameRes, _labelRes, _iconRes, _roundIconRes, _logoRes,
+ _bannerRes);
sepProcesses = _sepProcesses;
processRes = _processRes;
descriptionRes = _descriptionRes;
@@ -2506,12 +2511,12 @@
TypedArray sa = res.obtainAttributes(parser,
com.android.internal.R.styleable.AndroidManifestPermissionGroup);
-
if (!parsePackageItemInfo(owner, perm.info, outError,
- "<permission-group>", sa,
+ "<permission-group>", sa, true /*nameRequired*/,
com.android.internal.R.styleable.AndroidManifestPermissionGroup_name,
com.android.internal.R.styleable.AndroidManifestPermissionGroup_label,
com.android.internal.R.styleable.AndroidManifestPermissionGroup_icon,
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup_roundIcon,
com.android.internal.R.styleable.AndroidManifestPermissionGroup_logo,
com.android.internal.R.styleable.AndroidManifestPermissionGroup_banner)) {
sa.recycle();
@@ -2552,10 +2557,11 @@
com.android.internal.R.styleable.AndroidManifestPermission);
if (!parsePackageItemInfo(owner, perm.info, outError,
- "<permission>", sa,
+ "<permission>", sa, true /*nameRequired*/,
com.android.internal.R.styleable.AndroidManifestPermission_name,
com.android.internal.R.styleable.AndroidManifestPermission_label,
com.android.internal.R.styleable.AndroidManifestPermission_icon,
+ com.android.internal.R.styleable.AndroidManifestPermission_roundIcon,
com.android.internal.R.styleable.AndroidManifestPermission_logo,
com.android.internal.R.styleable.AndroidManifestPermission_banner)) {
sa.recycle();
@@ -2621,10 +2627,11 @@
com.android.internal.R.styleable.AndroidManifestPermissionTree);
if (!parsePackageItemInfo(owner, perm.info, outError,
- "<permission-tree>", sa,
+ "<permission-tree>", sa, true /*nameRequired*/,
com.android.internal.R.styleable.AndroidManifestPermissionTree_name,
com.android.internal.R.styleable.AndroidManifestPermissionTree_label,
com.android.internal.R.styleable.AndroidManifestPermissionTree_icon,
+ com.android.internal.R.styleable.AndroidManifestPermissionTree_roundIcon,
com.android.internal.R.styleable.AndroidManifestPermissionTree_logo,
com.android.internal.R.styleable.AndroidManifestPermissionTree_banner)) {
sa.recycle();
@@ -2671,6 +2678,7 @@
com.android.internal.R.styleable.AndroidManifestInstrumentation_name,
com.android.internal.R.styleable.AndroidManifestInstrumentation_label,
com.android.internal.R.styleable.AndroidManifestInstrumentation_icon,
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_roundIcon,
com.android.internal.R.styleable.AndroidManifestInstrumentation_logo,
com.android.internal.R.styleable.AndroidManifestInstrumentation_banner);
mParseInstrumentationArgs.tag = "<instrumentation>";
@@ -2736,10 +2744,21 @@
TypedArray sa = res.obtainAttributes(parser,
com.android.internal.R.styleable.AndroidManifestApplication);
- String name = sa.getNonConfigurationString(
- com.android.internal.R.styleable.AndroidManifestApplication_name, 0);
- if (name != null) {
- ai.className = buildClassName(pkgName, name, outError);
+ if (!parsePackageItemInfo(owner, ai, outError,
+ "<application>", sa, false /*nameRequired*/,
+ com.android.internal.R.styleable.AndroidManifestApplication_name,
+ com.android.internal.R.styleable.AndroidManifestApplication_label,
+ com.android.internal.R.styleable.AndroidManifestApplication_icon,
+ com.android.internal.R.styleable.AndroidManifestApplication_roundIcon,
+ com.android.internal.R.styleable.AndroidManifestApplication_logo,
+ com.android.internal.R.styleable.AndroidManifestApplication_banner)) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ if (ai.name != null) {
+ ai.className = buildClassName(pkgName, ai.name, outError);
if (ai.className == null) {
sa.recycle();
mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
@@ -2810,18 +2829,6 @@
}
}
- TypedValue v = sa.peekValue(
- com.android.internal.R.styleable.AndroidManifestApplication_label);
- if (v != null && (ai.labelRes=v.resourceId) == 0) {
- ai.nonLocalizedLabel = v.coerceToString();
- }
-
- ai.icon = sa.getResourceId(
- com.android.internal.R.styleable.AndroidManifestApplication_icon, 0);
- ai.logo = sa.getResourceId(
- com.android.internal.R.styleable.AndroidManifestApplication_logo, 0);
- ai.banner = sa.getResourceId(
- com.android.internal.R.styleable.AndroidManifestApplication_banner, 0);
ai.theme = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_theme, 0);
ai.descriptionRes = sa.getResourceId(
@@ -3335,25 +3342,35 @@
return true;
}
- private boolean parsePackageItemInfo(Package owner, PackageItemInfo outInfo,
- String[] outError, String tag, TypedArray sa,
- int nameRes, int labelRes, int iconRes, int logoRes, int bannerRes) {
+ private static boolean parsePackageItemInfo(Package owner, PackageItemInfo outInfo,
+ String[] outError, String tag, TypedArray sa, boolean nameRequired,
+ int nameRes, int labelRes, int iconRes, int roundIconRes, int logoRes, int bannerRes) {
String name = sa.getNonConfigurationString(nameRes, 0);
if (name == null) {
- outError[0] = tag + " does not specify android:name";
- return false;
+ if (nameRequired) {
+ outError[0] = tag + " does not specify android:name";
+ return false;
+ }
+ } else {
+ outInfo.name
+ = buildClassName(owner.applicationInfo.packageName, name, outError);
+ if (outInfo.name == null) {
+ return false;
+ }
}
- outInfo.name
- = buildClassName(owner.applicationInfo.packageName, name, outError);
- if (outInfo.name == null) {
- return false;
- }
-
- int iconVal = sa.getResourceId(iconRes, 0);
- if (iconVal != 0) {
- outInfo.icon = iconVal;
+ final boolean useRoundIcon =
+ Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useRoundIcon);
+ int roundIconVal = useRoundIcon ? sa.getResourceId(roundIconRes, 0) : 0;
+ if (roundIconVal != 0) {
+ outInfo.icon = roundIconVal;
outInfo.nonLocalizedLabel = null;
+ } else {
+ int iconVal = sa.getResourceId(iconRes, 0);
+ if (iconVal != 0) {
+ outInfo.icon = iconVal;
+ outInfo.nonLocalizedLabel = null;
+ }
}
int logoVal = sa.getResourceId(logoRes, 0);
@@ -3387,6 +3404,7 @@
R.styleable.AndroidManifestActivity_name,
R.styleable.AndroidManifestActivity_label,
R.styleable.AndroidManifestActivity_icon,
+ R.styleable.AndroidManifestActivity_roundIcon,
R.styleable.AndroidManifestActivity_logo,
R.styleable.AndroidManifestActivity_banner,
mSeparateProcesses,
@@ -3761,6 +3779,7 @@
com.android.internal.R.styleable.AndroidManifestActivityAlias_name,
com.android.internal.R.styleable.AndroidManifestActivityAlias_label,
com.android.internal.R.styleable.AndroidManifestActivityAlias_icon,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_roundIcon,
com.android.internal.R.styleable.AndroidManifestActivityAlias_logo,
com.android.internal.R.styleable.AndroidManifestActivityAlias_banner,
mSeparateProcesses,
@@ -3915,6 +3934,7 @@
com.android.internal.R.styleable.AndroidManifestProvider_name,
com.android.internal.R.styleable.AndroidManifestProvider_label,
com.android.internal.R.styleable.AndroidManifestProvider_icon,
+ com.android.internal.R.styleable.AndroidManifestProvider_roundIcon,
com.android.internal.R.styleable.AndroidManifestProvider_logo,
com.android.internal.R.styleable.AndroidManifestProvider_banner,
mSeparateProcesses,
@@ -4234,6 +4254,7 @@
com.android.internal.R.styleable.AndroidManifestService_name,
com.android.internal.R.styleable.AndroidManifestService_label,
com.android.internal.R.styleable.AndroidManifestService_icon,
+ com.android.internal.R.styleable.AndroidManifestService_roundIcon,
com.android.internal.R.styleable.AndroidManifestService_logo,
com.android.internal.R.styleable.AndroidManifestService_banner,
mSeparateProcesses,
@@ -4550,8 +4571,16 @@
outInfo.nonLocalizedLabel = v.coerceToString();
}
- outInfo.icon = sa.getResourceId(
- com.android.internal.R.styleable.AndroidManifestIntentFilter_icon, 0);
+ final boolean useRoundIcon =
+ Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useRoundIcon);
+ int roundIconVal = useRoundIcon ? sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestIntentFilter_roundIcon, 0) : 0;
+ if (roundIconVal != 0) {
+ outInfo.icon = roundIconVal;
+ } else {
+ outInfo.icon = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestIntentFilter_icon, 0);
+ }
outInfo.logo = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestIntentFilter_logo, 0);
@@ -5180,45 +5209,13 @@
public Component(final ParsePackageItemArgs args, final PackageItemInfo outInfo) {
owner = args.owner;
intents = new ArrayList<II>(0);
- String name = args.sa.getNonConfigurationString(args.nameRes, 0);
- if (name == null) {
+ if (parsePackageItemInfo(args.owner, outInfo, args.outError, args.tag, args.sa,
+ true /*nameRequired*/, args.nameRes, args.labelRes, args.iconRes,
+ args.roundIconRes, args.logoRes, args.bannerRes)) {
+ className = outInfo.name;
+ } else {
className = null;
- args.outError[0] = args.tag + " does not specify android:name";
- return;
}
-
- outInfo.name
- = buildClassName(owner.applicationInfo.packageName, name, args.outError);
- if (outInfo.name == null) {
- className = null;
- args.outError[0] = args.tag + " does not have valid android:name";
- return;
- }
-
- className = outInfo.name;
-
- int iconVal = args.sa.getResourceId(args.iconRes, 0);
- if (iconVal != 0) {
- outInfo.icon = iconVal;
- outInfo.nonLocalizedLabel = null;
- }
-
- int logoVal = args.sa.getResourceId(args.logoRes, 0);
- if (logoVal != 0) {
- outInfo.logo = logoVal;
- }
-
- int bannerVal = args.sa.getResourceId(args.bannerRes, 0);
- if (bannerVal != 0) {
- outInfo.banner = bannerVal;
- }
-
- TypedValue v = args.sa.peekValue(args.labelRes);
- if (v != null && (outInfo.labelRes=v.resourceId) == 0) {
- outInfo.nonLocalizedLabel = v.coerceToString();
- }
-
- outInfo.packageName = owner.packageName;
}
public Component(final ParseComponentArgs args, final ComponentInfo outInfo) {
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 4340d04..8492fd8 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -22,14 +22,19 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.ArraySet;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
@@ -50,26 +55,49 @@
* </ul>
*
* @see {@link ShortcutManager}.
- *
- * @hide
*/
public final class ShortcutInfo implements Parcelable {
- /* @hide */
+ static final String TAG = "Shortcut";
+
+ private static final String RES_TYPE_STRING = "string";
+
+ private static final String ANDROID_PACKAGE_NAME = "android";
+
+ private static final int IMPLICIT_RANK_MASK = 0x7fffffff;
+
+ private static final int RANK_CHANGED_BIT = ~IMPLICIT_RANK_MASK;
+
+ /** @hide */
+ public static final int RANK_NOT_SET = Integer.MAX_VALUE;
+
+ /** @hide */
public static final int FLAG_DYNAMIC = 1 << 0;
- /* @hide */
+ /** @hide */
public static final int FLAG_PINNED = 1 << 1;
- /* @hide */
+ /** @hide */
public static final int FLAG_HAS_ICON_RES = 1 << 2;
- /* @hide */
+ /** @hide */
public static final int FLAG_HAS_ICON_FILE = 1 << 3;
- /* @hide */
+ /** @hide */
public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4;
/** @hide */
+ public static final int FLAG_MANIFEST = 1 << 5;
+
+ /** @hide */
+ public static final int FLAG_DISABLED = 1 << 6;
+
+ /** @hide */
+ public static final int FLAG_STRINGS_RESOLVED = 1 << 7;
+
+ /** @hide */
+ public static final int FLAG_IMMUTABLE = 1 << 8;
+
+ /** @hide */
@IntDef(flag = true,
value = {
FLAG_DYNAMIC,
@@ -77,26 +105,34 @@
FLAG_HAS_ICON_RES,
FLAG_HAS_ICON_FILE,
FLAG_KEY_FIELDS_ONLY,
+ FLAG_MANIFEST,
+ FLAG_DISABLED,
+ FLAG_STRINGS_RESOLVED,
+ FLAG_IMMUTABLE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ShortcutFlags {}
// Cloning options.
- /* @hide */
+ /** @hide */
private static final int CLONE_REMOVE_ICON = 1 << 0;
- /* @hide */
+ /** @hide */
private static final int CLONE_REMOVE_INTENT = 1 << 1;
- /* @hide */
+ /** @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_RES_NAMES = 1 << 3;
- /* @hide */
- public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT;
+ /** @hide */
+ public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON | CLONE_REMOVE_RES_NAMES;
+
+ /** @hide */
+ public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT
+ | CLONE_REMOVE_RES_NAMES;
/** @hide */
@IntDef(flag = true,
@@ -104,6 +140,7 @@
CLONE_REMOVE_ICON,
CLONE_REMOVE_INTENT,
CLONE_REMOVE_NON_KEY_INFO,
+ CLONE_REMOVE_RES_NAMES,
CLONE_REMOVE_FOR_CREATOR,
CLONE_REMOVE_FOR_LAUNCHER
})
@@ -121,33 +158,57 @@
private final String mPackageName;
@Nullable
- private ComponentName mActivityComponent;
+ private ComponentName mActivity;
@Nullable
private Icon mIcon;
- @NonNull
- private String mTitle;
+ private int mTitleResId;
+
+ private String mTitleResName;
@Nullable
- private String mText;
+ private CharSequence mTitle;
- @NonNull
+ private int mTextResId;
+
+ private String mTextResName;
+
+ @Nullable
+ private CharSequence mText;
+
+ private int mDisabledMessageResId;
+
+ private String mDisabledMessageResName;
+
+ @Nullable
+ private CharSequence mDisabledMessage;
+
+ @Nullable
private ArraySet<String> mCategories;
/**
* Intent *with extras removed*.
*/
- @NonNull
+ @Nullable
private Intent mIntent;
/**
* Extras for the intent.
*/
- @NonNull
+ @Nullable
private PersistableBundle mIntentPersistableExtras;
- private int mWeight;
+ private int mRank;
+
+ /**
+ * Internally used for auto-rank-adjustment.
+ *
+ * RANK_CHANGED_BIT is used to denote that the rank of a shortcut is changing.
+ * The rest of the bits are used to denote the order in which shortcuts are passed to
+ * APIs, which is used to preserve the argument order when ranks are tie.
+ */
+ private int mImplicitRank;
@Nullable
private PersistableBundle mExtras;
@@ -159,7 +220,9 @@
private int mFlags;
// Internal use only.
- private int mIconResourceId;
+ private int mIconResId;
+
+ private String mIconResName;
// Internal use only.
@Nullable
@@ -175,11 +238,15 @@
// Note we can't do other null checks here because SM.updateShortcuts() takes partial
// information.
mPackageName = b.mContext.getPackageName();
- mActivityComponent = b.mActivityComponent;
+ mActivity = b.mActivity;
mIcon = b.mIcon;
mTitle = b.mTitle;
+ mTitleResId = b.mTitleResId;
mText = b.mText;
- mCategories = clone(b.mCategories);
+ mTextResId = b.mTextResId;
+ mDisabledMessage = b.mDisabledMessage;
+ mDisabledMessageResId = b.mDisabledMessageResId;
+ mCategories = cloneCategories(b.mCategories);
mIntent = b.mIntent;
if (mIntent != null) {
final Bundle intentExtras = mIntent.getExtras();
@@ -188,13 +255,22 @@
mIntentPersistableExtras = new PersistableBundle(intentExtras);
}
}
- mWeight = b.mWeight;
+ mRank = b.mRank;
mExtras = b.mExtras;
updateTimestamp();
}
- private <T> ArraySet<T> clone(Set<T> source) {
- return (source == null) ? null : new ArraySet<>(source);
+ private ArraySet<String> cloneCategories(Set<String> source) {
+ if (source == null) {
+ return null;
+ }
+ final ArraySet<String> ret = new ArraySet<>(source.size());
+ for (CharSequence s : source) {
+ if (!TextUtils.isEmpty(s)) {
+ ret.add(s.toString().intern());
+ }
+ }
+ return ret;
}
/**
@@ -204,7 +280,10 @@
*/
public void enforceMandatoryFields() {
Preconditions.checkStringNotEmpty(mId, "Shortcut ID must be provided");
- Preconditions.checkStringNotEmpty(mTitle, "Shortcut title must be provided");
+ Preconditions.checkNotNull(mActivity, "Activity must be provided");
+ if (mTitle == null && mTitleResId == 0) {
+ throw new IllegalArgumentException("Short label must be provided");
+ }
Preconditions.checkNotNull(mIntent, "Shortcut Intent must be provided");
}
@@ -215,14 +294,14 @@
mUserId = source.mUserId;
mId = source.mId;
mPackageName = source.mPackageName;
+ mActivity = source.mActivity;
mFlags = source.mFlags;
mLastChangedTimestamp = source.mLastChangedTimestamp;
// Just always keep it since it's cheep.
- mIconResourceId = source.mIconResourceId;
+ mIconResId = source.mIconResId;
if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) {
- mActivityComponent = source.mActivityComponent;
if ((cloneFlags & CLONE_REMOVE_ICON) == 0) {
mIcon = source.mIcon;
@@ -230,14 +309,25 @@
}
mTitle = source.mTitle;
+ mTitleResId = source.mTitleResId;
mText = source.mText;
- mCategories = clone(source.mCategories);
+ mTextResId = source.mTextResId;
+ mDisabledMessage = source.mDisabledMessage;
+ mDisabledMessageResId = source.mDisabledMessageResId;
+ mCategories = cloneCategories(source.mCategories);
if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) {
mIntent = source.mIntent;
mIntentPersistableExtras = source.mIntentPersistableExtras;
}
- mWeight = source.mWeight;
+ mRank = source.mRank;
mExtras = source.mExtras;
+
+ if ((cloneFlags & CLONE_REMOVE_RES_NAMES) == 0) {
+ mTitleResName = source.mTitleResName;
+ mTextResName = source.mTextResName;
+ mDisabledMessageResName = source.mDisabledMessageResName;
+ mIconResName = source.mIconResName;
+ }
} else {
// Set this bit.
mFlags |= FLAG_KEY_FIELDS_ONLY;
@@ -245,6 +335,221 @@
}
/**
+ * Load a string resource from the publisher app.
+ *
+ * @param resId resource ID
+ * @param defValue default value to be returned when the specified resource isn't found.
+ */
+ private CharSequence getResourceString(Resources res, int resId, CharSequence defValue) {
+ try {
+ return res.getString(resId);
+ } catch (NotFoundException e) {
+ Log.e(TAG, "Resource for ID=" + resId + " not found in package " + mPackageName);
+ return defValue;
+ }
+ }
+
+ /**
+ * Load the string resources for the text fields and set them to the actual value fields.
+ * This will set {@link #FLAG_STRINGS_RESOLVED}.
+ *
+ * @param res {@link Resources} for the publisher. Must have been loaded with
+ * {@link PackageManager#getResourcesForApplicationAsUser}.
+ *
+ * @hide
+ */
+ public void resolveResourceStrings(@NonNull Resources res) {
+ mFlags |= FLAG_STRINGS_RESOLVED;
+
+ if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)) {
+ return; // Bail early.
+ }
+
+ if (mTitleResId != 0) {
+ mTitle = getResourceString(res, mTitleResId, mTitle);
+ }
+ if (mTextResId != 0) {
+ mText = getResourceString(res, mTextResId, mText);
+ }
+ if (mDisabledMessageResId != 0) {
+ mDisabledMessage = getResourceString(res, mDisabledMessageResId, mDisabledMessage);
+ }
+ }
+
+ /**
+ * Look up resource name for a given resource ID.
+ *
+ * @return a simple resource name (e.g. "text_1") when {@code withType} is false, or with the
+ * type (e.g. "string/text_1").
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static String lookUpResourceName(@NonNull Resources res, int resId, boolean withType,
+ @NonNull String packageName) {
+ if (resId == 0) {
+ return null;
+ }
+ try {
+ final String fullName = res.getResourceName(resId);
+
+ if (ANDROID_PACKAGE_NAME.equals(getResourcePackageName(fullName))) {
+ // If it's a framework resource, the value won't change, so just return the ID
+ // value as a string.
+ return String.valueOf(resId);
+ }
+ return withType ? getResourceTypeAndEntryName(fullName)
+ : getResourceEntryName(fullName);
+ } catch (NotFoundException e) {
+ Log.e(TAG, "Resource name for ID=" + resId + " not found in package " + packageName
+ + ". Resource IDs may change when the application is upgraded, and the system"
+ + " may not be able to find the correct resource.");
+ return null;
+ }
+ }
+
+ /**
+ * Extract the package name from a fully-donated resource name.
+ * e.g. "com.android.app1:drawable/icon1" -> "com.android.app1"
+ * @hide
+ */
+ @VisibleForTesting
+ public static String getResourcePackageName(@NonNull String fullResourceName) {
+ final int p1 = fullResourceName.indexOf(':');
+ if (p1 < 0) {
+ return null;
+ }
+ return fullResourceName.substring(0, p1);
+ }
+
+ /**
+ * Extract the type name from a fully-donated resource name.
+ * e.g. "com.android.app1:drawable/icon1" -> "drawable"
+ * @hide
+ */
+ @VisibleForTesting
+ public static String getResourceTypeName(@NonNull String fullResourceName) {
+ final int p1 = fullResourceName.indexOf(':');
+ if (p1 < 0) {
+ return null;
+ }
+ final int p2 = fullResourceName.indexOf('/', p1 + 1);
+ if (p2 < 0) {
+ return null;
+ }
+ return fullResourceName.substring(p1 + 1, p2);
+ }
+
+ /**
+ * Extract the type name + the entry name from a fully-donated resource name.
+ * e.g. "com.android.app1:drawable/icon1" -> "drawable/icon1"
+ * @hide
+ */
+ @VisibleForTesting
+ public static String getResourceTypeAndEntryName(@NonNull String fullResourceName) {
+ final int p1 = fullResourceName.indexOf(':');
+ if (p1 < 0) {
+ return null;
+ }
+ return fullResourceName.substring(p1 + 1);
+ }
+
+ /**
+ * Extract the entry name from a fully-donated resource name.
+ * e.g. "com.android.app1:drawable/icon1" -> "icon1"
+ * @hide
+ */
+ @VisibleForTesting
+ public static String getResourceEntryName(@NonNull String fullResourceName) {
+ final int p1 = fullResourceName.indexOf('/');
+ if (p1 < 0) {
+ return null;
+ }
+ return fullResourceName.substring(p1 + 1);
+ }
+
+ /**
+ * Return the resource ID for a given resource ID.
+ *
+ * Basically its' a wrapper over {@link Resources#getIdentifier(String, String, String)}, except
+ * if {@code resourceName} is an integer then it'll just return its value. (Which also the
+ * aforementioned method would do internally, but not documented, so doing here explicitly.)
+ *
+ * @param res {@link Resources} for the publisher. Must have been loaded with
+ * {@link PackageManager#getResourcesForApplicationAsUser}.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static int lookUpResourceId(@NonNull Resources res, @Nullable String resourceName,
+ @Nullable String resourceType, String packageName) {
+ if (resourceName == null) {
+ return 0;
+ }
+ try {
+ try {
+ // It the name can be parsed as an integer, just use it.
+ return Integer.parseInt(resourceName);
+ } catch (NumberFormatException ignore) {
+ }
+
+ return res.getIdentifier(resourceName, resourceType, packageName);
+ } catch (NotFoundException e) {
+ Log.e(TAG, "Resource ID for name=" + resourceName + " not found in package "
+ + packageName);
+ return 0;
+ }
+ }
+
+ /**
+ * Look up resource names from the resource IDs for the icon res and the text fields, and fill
+ * in the resource name fields.
+ *
+ * @param res {@link Resources} for the publisher. Must have been loaded with
+ * {@link PackageManager#getResourcesForApplicationAsUser}.
+ *
+ * @hide
+ */
+ public void lookupAndFillInResourceNames(@NonNull Resources res) {
+ if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)
+ && (mIconResId == 0)) {
+ return; // Bail early.
+ }
+
+ // We don't need types for strings because their types are always "string".
+ mTitleResName = lookUpResourceName(res, mTitleResId, /*withType=*/ false, mPackageName);
+ mTextResName = lookUpResourceName(res, mTextResId, /*withType=*/ false, mPackageName);
+ mDisabledMessageResName = lookUpResourceName(res, mDisabledMessageResId,
+ /*withType=*/ false, mPackageName);
+
+ // But icons have multiple possible types, so include the type.
+ mIconResName = lookUpResourceName(res, mIconResId, /*withType=*/ true, mPackageName);
+ }
+
+ /**
+ * Look up resource IDs from the resource names for the icon res and the text fields, and fill
+ * in the resource ID fields.
+ *
+ * This is called when an app is updated.
+ *
+ * @hide
+ */
+ public void lookupAndFillInResourceIds(@NonNull Resources res) {
+ if ((mTitleResName == null) && (mTextResName == null) && (mDisabledMessageResName == null)
+ && (mIconResName == null)) {
+ return; // Bail early.
+ }
+
+ mTitleResId = lookUpResourceId(res, mTitleResName, RES_TYPE_STRING, mPackageName);
+ mTextResId = lookUpResourceId(res, mTextResName, RES_TYPE_STRING, mPackageName);
+ mDisabledMessageResId = lookUpResourceId(res, mDisabledMessageResName, RES_TYPE_STRING,
+ mPackageName);
+
+ // mIconResName already contains the type, so the third argument is not needed.
+ mIconResId = lookUpResourceId(res, mIconResName, null, mPackageName);
+ }
+
+ /**
* Copy a {@link ShortcutInfo}, optionally removing fields.
* @hide
*/
@@ -253,49 +558,84 @@
}
/**
+ * @hide
+ */
+ public void ensureUpdatableWith(ShortcutInfo source) {
+ Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match");
+ Preconditions.checkState(mId.equals(source.mId), "ID must match");
+ Preconditions.checkState(mPackageName.equals(source.mPackageName),
+ "Package name must match");
+ Preconditions.checkState(!isImmutable(), "Target ShortcutInfo is immutable");
+ }
+
+ /**
* Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information
- * will be overwritten. The timestamp will be updated.
+ * will be overwritten. The timestamp will *not* be updated to be consistent with other
+ * setters (and also the clock is not injectable in this file).
*
* - Flags will not change
* - mBitmapPath will not change
* - Current time will be set to timestamp
*
+ * @throws IllegalStateException if source is not compatible.
+ *
* @hide
*/
public void copyNonNullFieldsFrom(ShortcutInfo source) {
- Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match");
- Preconditions.checkState(mId.equals(source.mId), "ID must match");
- Preconditions.checkState(mPackageName.equals(source.mPackageName),
- "Package name must match");
+ ensureUpdatableWith(source);
- if (source.mActivityComponent != null) {
- mActivityComponent = source.mActivityComponent;
+ if (source.mActivity != null) {
+ mActivity = source.mActivity;
}
if (source.mIcon != null) {
mIcon = source.mIcon;
+
+ mIconResId = 0;
+ mIconResName = null;
+ mBitmapPath = null;
}
if (source.mTitle != null) {
mTitle = source.mTitle;
+ mTitleResId = 0;
+ mTitleResName = null;
+ } else if (source.mTitleResId != 0) {
+ mTitle = null;
+ mTitleResId = source.mTitleResId;
+ mTitleResName = null;
}
+
if (source.mText != null) {
mText = source.mText;
+ mTextResId = 0;
+ mTextResName = null;
+ } else if (source.mTextResId != 0) {
+ mText = null;
+ mTextResId = source.mTextResId;
+ mTextResName = null;
+ }
+ if (source.mDisabledMessage != null) {
+ mDisabledMessage = source.mDisabledMessage;
+ mDisabledMessageResId = 0;
+ mDisabledMessageResName = null;
+ } else if (source.mDisabledMessageResId != 0) {
+ mDisabledMessage = null;
+ mDisabledMessageResId = source.mDisabledMessageResId;
+ mDisabledMessageResName = null;
}
if (source.mCategories != null) {
- mCategories = clone(source.mCategories);
+ mCategories = cloneCategories(source.mCategories);
}
if (source.mIntent != null) {
mIntent = source.mIntent;
mIntentPersistableExtras = source.mIntentPersistableExtras;
}
- if (source.mWeight != 0) {
- mWeight = source.mWeight;
+ if (source.mRank != RANK_NOT_SET) {
+ mRank = source.mRank;
}
if (source.mExtras != null) {
mExtras = source.mExtras;
}
-
- updateTimestamp();
}
/**
@@ -310,7 +650,6 @@
throw getInvalidIconException();
}
if (icon.hasTint()) {
- // TODO support it
throw new IllegalArgumentException("Icons with tints are not supported");
}
@@ -331,19 +670,27 @@
private String mId;
- private ComponentName mActivityComponent;
+ private ComponentName mActivity;
private Icon mIcon;
- private String mTitle;
+ private int mTitleResId;
- private String mText;
+ private CharSequence mTitle;
+
+ private int mTextResId;
+
+ private CharSequence mText;
+
+ private int mDisabledMessageResId;
+
+ private CharSequence mDisabledMessage;
private Set<String> mCategories;
private Intent mIntent;
- private int mWeight;
+ private int mRank = RANK_NOT_SET;
private PersistableBundle mExtras;
@@ -373,8 +720,8 @@
* a hint to the launcher app about which launcher icon to associate this shortcut with.
*/
@NonNull
- public Builder setActivityComponent(@NonNull ComponentName activityComponent) {
- mActivityComponent = Preconditions.checkNotNull(activityComponent, "activityComponent");
+ public Builder setActivity(@NonNull ComponentName activity) {
+ mActivity = Preconditions.checkNotNull(activity, "activity");
return this;
}
@@ -399,26 +746,88 @@
}
/**
- * Sets the title of a shortcut. This is a mandatory field.
+ * @hide We don't support resource strings for dynamic shortcuts for now. (But unit tests
+ * use it.)
+ */
+ public Builder setShortLabelResId(int shortLabelResId) {
+ Preconditions.checkState(mTitle == null, "shortLabel already set");
+ mTitleResId = shortLabelResId;
+ return this;
+ }
+
+ /**
+ * Sets the short title of a shortcut. This is a mandatory field.
*
* <p>This field is intended for a concise description of a shortcut displayed under
* an icon. The recommend max length is 10 characters.
*/
@NonNull
- public Builder setTitle(@NonNull String title) {
- mTitle = Preconditions.checkStringNotEmpty(title, "title");
+ public Builder setShortLabel(@NonNull CharSequence shortLabel) {
+ Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set");
+ mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel");
+ return this;
+ }
+
+ /**
+ * @hide We don't support resource strings for dynamic shortcuts for now. (But unit tests
+ * use it.)
+ */
+ public Builder setLongLabelResId(int longLabelResId) {
+ Preconditions.checkState(mText == null, "longLabel already set");
+ mTextResId = longLabelResId;
return this;
}
/**
* Sets the text of a shortcut. This is an optional field.
*
- * <p>This field is intended to be more descriptive than the shortcut title.
+ * <p>This field is intended to be more descriptive than the shortcut title. The launcher
+ * shows this instead of the short title, when it has enough space.
* The recommend max length is 25 characters.
*/
@NonNull
- public Builder setText(@NonNull String text) {
- mText = Preconditions.checkStringNotEmpty(text, "text");
+ public Builder setLongLabel(@NonNull CharSequence longLabel) {
+ Preconditions.checkState(mTextResId == 0, "longLabelResId already set");
+ mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel");
+ return this;
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public Builder setTitle(@NonNull CharSequence value) {
+ return setShortLabel(value);
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public Builder setTitleResId(int value) {
+ return setShortLabelResId(value);
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public Builder setText(@NonNull CharSequence value) {
+ return setLongLabel(value);
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public Builder setTextResId(int value) {
+ return setLongLabelResId(value);
+ }
+
+ /**
+ * @hide We don't support resource strings for dynamic shortcuts for now. (But unit tests
+ * use it.)
+ */
+ public Builder setDisabledMessageResId(int disabledMessageResId) {
+ Preconditions.checkState(mDisabledMessage == null, "disabledMessage already set");
+ mDisabledMessageResId = disabledMessageResId;
+ return this;
+ }
+
+ @NonNull
+ public Builder setDisabledMessage(@NonNull CharSequence disabledMessage) {
+ Preconditions.checkState(
+ mDisabledMessageResId == 0, "disabledMessageResId already set");
+ mDisabledMessage =
+ Preconditions.checkStringNotEmpty(disabledMessage, "disabledMessage");
return this;
}
@@ -441,16 +850,19 @@
@NonNull
public Builder setIntent(@NonNull Intent intent) {
mIntent = Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set");
return this;
}
/**
- * Optionally sets the weight of a shortcut, which will be used by the launcher for sorting.
- * The larger the weight, the more "important" a shortcut is.
+ * "Rank" of a shortcut, which is a non-negative value that's used by the launcher app
+ * to sort shortcuts.
*/
@NonNull
- public Builder setWeight(int weight) {
- mWeight = weight;
+ public Builder setRank(int rank) {
+ Preconditions.checkArgument((0 <= rank),
+ "Rank cannot be negative or bigger than MAX_RANK");
+ mRank = rank;
return this;
}
@@ -485,7 +897,7 @@
* Return the package name of the creator application.
*/
@NonNull
- public String getPackageName() {
+ public String getPackage() {
return mPackageName;
}
@@ -496,11 +908,11 @@
* <p>This has nothing to do with the activity that this shortcut will launch. This is
* a hint to the launcher app that on which launcher icon this shortcut should be shown.
*
- * @see Builder#setActivityComponent
+ * @see Builder#setActivity
*/
@Nullable
- public ComponentName getActivityComponent() {
- return mActivityComponent;
+ public ComponentName getActivity() {
+ return mActivity;
}
/**
@@ -517,25 +929,70 @@
return mIcon;
}
+ /** @hide -- old signature, the internal code still uses it. */
+ @Nullable
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public int getTitleResId() {
+ return mTitleResId;
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ @Nullable
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /** @hide -- old signature, the internal code still uses it. */
+ public int getTextResId() {
+ return mTextResId;
+ }
+
/**
- * Return the shortcut title.
+ * Return the shorter version of the shortcut title.
*
* <p>All shortcuts must have a non-empty title, but this method will return null when
* {@link #hasKeyFieldsOnly()} is true.
*/
@Nullable
- public String getTitle() {
+ public CharSequence getShortLabel() {
return mTitle;
}
+ /** TODO Javadoc */
+ public int getShortLabelResourceId() {
+ return mTitleResId;
+ }
+
/**
* Return the shortcut text.
*/
@Nullable
- public String getText() {
+ public CharSequence getLongLabel() {
return mText;
}
+ /** TODO Javadoc */
+ public int getLongLabelResourceId() {
+ return mTextResId;
+ }
+
+ /**
+ * Return the message that should be shown when a shortcut in disabled state is launched.
+ */
+ @Nullable
+ public CharSequence getDisabledMessage() {
+ return mDisabledMessage;
+ }
+
+ /** TODO Javadoc */
+ public int getDisabledMessageResourceId() {
+ return mDisabledMessageResId;
+ }
+
/**
* Return the categories.
*/
@@ -585,11 +1042,55 @@
}
/**
- * Return the weight of a shortcut, which will be used by Launcher for sorting.
- * The larger the weight, the more "important" a shortcut is.
+ * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each
+ * {@link #getActivity} for each of the two kinds, dynamic shortcuts and manifest shortcuts.
+ *
+ * <p>Because manifest shortcuts and dynamic shortcuts have overlapping ranks,
+ * when a launcher application shows shortcuts for an activity, it should first show
+ * the manifest shortcuts followed by the dynamic shortcuts. Within each of those categories,
+ * shortcuts should be sorted by rank in ascending order.
+ *
+ * <p>"Floating" shortcuts (i.e. shortcuts that are neither dynamic nor manifest) will all
+ * have rank 0, because there's no sorting for them.
*/
- public int getWeight() {
- return mWeight;
+ public int getRank() {
+ return mRank;
+ }
+
+ /** @hide */
+ public boolean hasRank() {
+ return mRank != RANK_NOT_SET;
+ }
+
+ /** @hide */
+ public void setRank(int rank) {
+ mRank = rank;
+ }
+
+ /** @hide */
+ public void clearImplicitRankAndRankChangedFlag() {
+ mImplicitRank = 0;
+ }
+
+ /** @hide */
+ public void setImplicitRank(int rank) {
+ // Make sure to keep RANK_CHANGED_BIT.
+ mImplicitRank = (mImplicitRank & RANK_CHANGED_BIT) | (rank & IMPLICIT_RANK_MASK);
+ }
+
+ /** @hide */
+ public int getImplicitRank() {
+ return mImplicitRank & IMPLICIT_RANK_MASK;
+ }
+
+ /** @hide */
+ public void setRankChanged() {
+ mImplicitRank |= RANK_CHANGED_BIT;
+ }
+
+ /** @hide */
+ public boolean isRankChanged() {
+ return (mImplicitRank & RANK_CHANGED_BIT) != 0;
}
/**
@@ -656,6 +1157,68 @@
}
/**
+ * Return whether a shortcut is published via AndroidManifest.xml or not. If {@code true},
+ * it's also {@link #isImmutable()}.
+ *
+ * <p>When an app is upgraded and a shortcut is no longer published from AndroidManifest.xml,
+ * this will be set to {@code false}. If the shortcut is not pinned, then it'll just disappear.
+ * However, if it's pinned, it will still be alive, and {@link #isEnabled()} will be
+ * {@code false} and {@link #isImmutable()} will be {@code true}.
+ *
+ * <p>NOTE this is whether a shortcut is published from the <b>current version's</b>
+ * AndroidManifest.xml.
+ */
+ public boolean isManifestShortcut() {
+ return hasFlags(FLAG_MANIFEST);
+ }
+
+ /**
+ * @return true if pinned but neither dynamic nor manifest.
+ * @hide
+ */
+ public boolean isFloating() {
+ return isPinned() && !(isDynamic() || isManifestShortcut());
+ }
+
+ /** @hide */
+ public boolean isOriginallyFromManifest() {
+ return hasFlags(FLAG_IMMUTABLE);
+ }
+
+ /**
+ * Return if a shortcut is immutable, in which case it cannot be modified with any of
+ * {@link ShortcutManager} APIs.
+ *
+ * <p>All manifest shortcuts are immutable. When a manifest shortcut is pinned and then
+ * disabled because the app is upgraded and its AndroidManifest.xml no longer publishes it,
+ * {@link #isManifestShortcut} returns {@code false}, but it is still immutable.
+ *
+ * <p>All shortcuts originally published via the {@link ShortcutManager} APIs
+ * are all mutable.
+ */
+ public boolean isImmutable() {
+ return hasFlags(FLAG_IMMUTABLE);
+ }
+
+ /**
+ * Returns {@code false} if a shortcut is disabled with
+ * {@link ShortcutManager#disableShortcuts}.
+ */
+ public boolean isEnabled() {
+ return !hasFlags(FLAG_DISABLED);
+ }
+
+ /** @hide */
+ public boolean isAlive() {
+ return hasFlags(FLAG_PINNED) || hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST);
+ }
+
+ /** @hide */
+ public boolean usesQuota() {
+ return hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST);
+ }
+
+ /**
* Return whether a shortcut's icon is a resource in the owning package.
*
* @see LauncherApps#getShortcutIconResId(ShortcutInfo)
@@ -664,6 +1227,16 @@
return hasFlags(FLAG_HAS_ICON_RES);
}
+ /** @hide */
+ public boolean hasStringResources() {
+ return (mTitleResId != 0) || (mTextResId != 0) || (mDisabledMessageResId != 0);
+ }
+
+ /** @hide */
+ public boolean hasAnyResources() {
+ return hasIconResource() || hasStringResources();
+ }
+
/**
* Return whether a shortcut's icon is stored as a file.
*
@@ -678,7 +1251,8 @@
* following fields are available.
* <ul>
* <li>{@link #getId()}
- * <li>{@link #getPackageName()}
+ * <li>{@link #getPackage()}
+ * <li>{@link #getActivity()}
* <li>{@link #getLastChangedTimestamp()}
* <li>{@link #isDynamic()}
* <li>{@link #isPinned()}
@@ -690,6 +1264,11 @@
return hasFlags(FLAG_KEY_FIELDS_ONLY);
}
+ /** TODO Javadoc */
+ public boolean hasStringResourcesResolved() {
+ return hasFlags(FLAG_STRINGS_RESOLVED);
+ }
+
/** @hide */
public void updateTimestamp() {
mLastChangedTimestamp = System.currentTimeMillis();
@@ -708,14 +1287,17 @@
/** @hide */
public void setIconResourceId(int iconResourceId) {
- mIconResourceId = iconResourceId;
+ if (mIconResId != iconResourceId) {
+ mIconResName = null;
+ }
+ mIconResId = iconResourceId;
}
/**
* Get the resource ID for the icon, valid only when {@link #hasIconResource()} } is true.
*/
public int getIconResourceId() {
- return mIconResourceId;
+ return mIconResId;
}
/** @hide */
@@ -728,25 +1310,126 @@
mBitmapPath = bitmapPath;
}
+ /** @hide */
+ public void setDisabledMessageResId(int disabledMessageResId) {
+ if (mDisabledMessageResId != disabledMessageResId) {
+ mDisabledMessageResName = null;
+ }
+ mDisabledMessageResId = disabledMessageResId;
+ mDisabledMessage = null;
+ }
+
+ /** @hide */
+ public void setDisabledMessage(String disabledMessage) {
+ mDisabledMessage = disabledMessage;
+ mDisabledMessageResId = 0;
+ mDisabledMessageResName = null;
+ }
+
+ /** @hide */
+ public String getTitleResName() {
+ return mTitleResName;
+ }
+
+ /** @hide */
+ public void setTitleResName(String titleResName) {
+ mTitleResName = titleResName;
+ }
+
+ /** @hide */
+ public String getTextResName() {
+ return mTextResName;
+ }
+
+ /** @hide */
+ public void setTextResName(String textResName) {
+ mTextResName = textResName;
+ }
+
+ /** @hide */
+ public String getDisabledMessageResName() {
+ return mDisabledMessageResName;
+ }
+
+ /** @hide */
+ public void setDisabledMessageResName(String disabledMessageResName) {
+ mDisabledMessageResName = disabledMessageResName;
+ }
+
+ /** @hide */
+ public String getIconResName() {
+ return mIconResName;
+ }
+
+ /** @hide */
+ public void setIconResName(String iconResName) {
+ mIconResName = iconResName;
+ }
+
+ /**
+ * Replaces the intent
+ *
+ * @throws IllegalArgumentException when extra is not compatible with {@link PersistableBundle}.
+ *
+ * @hide
+ */
+ public void setIntent(Intent intent) throws IllegalArgumentException {
+ Preconditions.checkNotNull(intent);
+
+ final Bundle intentExtras = intent.getExtras();
+
+ mIntent = intent;
+
+ if (intentExtras != null) {
+ intent.replaceExtras((Bundle) null);
+ mIntentPersistableExtras = new PersistableBundle(intentExtras);
+ } else {
+ mIntentPersistableExtras = null;
+ }
+ }
+
+ /**
+ * Replaces the categories.
+ *
+ * @hide
+ */
+ public void setCategories(Set<String> categories) {
+ mCategories = cloneCategories(categories);
+ }
+
private ShortcutInfo(Parcel source) {
final ClassLoader cl = getClass().getClassLoader();
mUserId = source.readInt();
mId = source.readString();
mPackageName = source.readString();
- mActivityComponent = source.readParcelable(cl);
+ mActivity = source.readParcelable(cl);
+ mFlags = source.readInt();
+ mIconResId = source.readInt();
+ mLastChangedTimestamp = source.readLong();
+
+ if (source.readInt() == 0) {
+ return; // key information only.
+ }
+
mIcon = source.readParcelable(cl);
- mTitle = source.readString();
- mText = source.readString();
+ mTitle = source.readCharSequence();
+ mTitleResId = source.readInt();
+ mText = source.readCharSequence();
+ mTextResId = source.readInt();
+ mDisabledMessage = source.readCharSequence();
+ mDisabledMessageResId = source.readInt();
mIntent = source.readParcelable(cl);
mIntentPersistableExtras = source.readParcelable(cl);
- mWeight = source.readInt();
+ mRank = source.readInt();
mExtras = source.readParcelable(cl);
- mLastChangedTimestamp = source.readLong();
- mFlags = source.readInt();
- mIconResourceId = source.readInt();
mBitmapPath = source.readString();
+ mIconResName = source.readString();
+ mTitleResName = source.readString();
+ mTextResName = source.readString();
+ mDisabledMessageResName = source.readString();
+
int N = source.readInt();
if (N == 0) {
mCategories = null;
@@ -763,20 +1446,36 @@
dest.writeInt(mUserId);
dest.writeString(mId);
dest.writeString(mPackageName);
- dest.writeParcelable(mActivityComponent, flags);
+ dest.writeParcelable(mActivity, flags);
+ dest.writeInt(mFlags);
+ dest.writeInt(mIconResId);
+ dest.writeLong(mLastChangedTimestamp);
+
+ if (hasKeyFieldsOnly()) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(1);
+
dest.writeParcelable(mIcon, flags);
- dest.writeString(mTitle);
- dest.writeString(mText);
+ dest.writeCharSequence(mTitle);
+ dest.writeInt(mTitleResId);
+ dest.writeCharSequence(mText);
+ dest.writeInt(mTextResId);
+ dest.writeCharSequence(mDisabledMessage);
+ dest.writeInt(mDisabledMessageResId);
dest.writeParcelable(mIntent, flags);
dest.writeParcelable(mIntentPersistableExtras, flags);
- dest.writeInt(mWeight);
+ dest.writeInt(mRank);
dest.writeParcelable(mExtras, flags);
- dest.writeLong(mLastChangedTimestamp);
- dest.writeInt(mFlags);
- dest.writeInt(mIconResourceId);
dest.writeString(mBitmapPath);
+ dest.writeString(mIconResName);
+ dest.writeString(mTitleResName);
+ dest.writeString(mTextResName);
+ dest.writeString(mDisabledMessageResName);
+
if (mCategories != null) {
final int N = mCategories.size();
dest.writeInt(N);
@@ -823,24 +1522,67 @@
sb.append("id=");
sb.append(secure ? "***" : mId);
+ sb.append(", flags=0x");
+ sb.append(Integer.toHexString(mFlags));
+ sb.append(" [");
+ if (!isEnabled()) {
+ sb.append("X");
+ }
+ if (isImmutable()) {
+ sb.append("Im");
+ }
+ if (isManifestShortcut()) {
+ sb.append("M");
+ }
+ if (isDynamic()) {
+ sb.append("D");
+ }
+ if (isPinned()) {
+ sb.append("P");
+ }
+ if (hasIconFile()) {
+ sb.append("If");
+ }
+ if (hasIconResource()) {
+ sb.append("Ir");
+ }
+ if (hasKeyFieldsOnly()) {
+ sb.append("K");
+ }
+ if (hasStringResourcesResolved()) {
+ sb.append("Sr");
+ }
+ sb.append("]");
+
sb.append(", packageName=");
sb.append(mPackageName);
- if (isDynamic()) {
- sb.append(", dynamic");
- }
- if (isPinned()) {
- sb.append(", pinned");
- }
-
sb.append(", activity=");
- sb.append(mActivityComponent);
+ sb.append(mActivity);
- sb.append(", title=");
+ sb.append(", shortLabel=");
sb.append(secure ? "***" : mTitle);
+ sb.append(", resId=");
+ sb.append(mTitleResId);
+ sb.append("[");
+ sb.append(mTitleResName);
+ sb.append("]");
- sb.append(", text=");
+ sb.append(", longLabel=");
sb.append(secure ? "***" : mText);
+ sb.append(", resId=");
+ sb.append(mTextResId);
+ sb.append("[");
+ sb.append(mTextResName);
+ sb.append("]");
+
+ sb.append(", disabledMessage=");
+ sb.append(secure ? "***" : mDisabledMessage);
+ sb.append(", resId=");
+ sb.append(mDisabledMessageResId);
+ sb.append("[");
+ sb.append(mDisabledMessageResName);
+ sb.append("]");
sb.append(", categories=");
sb.append(mCategories);
@@ -848,8 +1590,8 @@
sb.append(", icon=");
sb.append(mIcon);
- sb.append(", weight=");
- sb.append(mWeight);
+ sb.append(", rank=");
+ sb.append(mRank);
sb.append(", timestamp=");
sb.append(mLastChangedTimestamp);
@@ -863,13 +1605,13 @@
sb.append(", extras=");
sb.append(mExtras);
- sb.append(", flags=");
- sb.append(mFlags);
-
if (includeInternalData) {
sb.append(", iconRes=");
- sb.append(mIconResourceId);
+ sb.append(mIconResId);
+ sb.append("[");
+ sb.append(mIconResName);
+ sb.append("]");
sb.append(", bitmapPath=");
sb.append(mBitmapPath);
@@ -881,26 +1623,37 @@
/** @hide */
public ShortcutInfo(
- @UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
- Icon icon, String title, String text, Set<String> categories, Intent intent,
- PersistableBundle intentPersistableExtras,
- int weight, PersistableBundle extras, long lastChangedTimestamp,
- int flags, int iconResId, String bitmapPath) {
+ @UserIdInt int userId, String id, String packageName, ComponentName activity,
+ Icon icon, CharSequence title, int titleResId, String titleResName,
+ CharSequence text, int textResId, String textResName,
+ CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
+ Set<String> categories,
+ Intent intent, PersistableBundle intentPersistableExtras,
+ int rank, PersistableBundle extras, long lastChangedTimestamp,
+ int flags, int iconResId, String iconResName, String bitmapPath) {
mUserId = userId;
mId = id;
mPackageName = packageName;
- mActivityComponent = activityComponent;
+ mActivity = activity;
mIcon = icon;
mTitle = title;
+ mTitleResId = titleResId;
+ mTitleResName = titleResName;
mText = text;
- mCategories = clone(categories);
+ mTextResId = textResId;
+ mTextResName = textResName;
+ mDisabledMessage = disabledMessage;
+ mDisabledMessageResId = disabledMessageResId;
+ mDisabledMessageResName = disabledMessageResName;
+ mCategories = cloneCategories(categories);
mIntent = intent;
mIntentPersistableExtras = intentPersistableExtras;
- mWeight = weight;
+ mRank = rank;
mExtras = extras;
mLastChangedTimestamp = lastChangedTimestamp;
mFlags = flags;
- mIconResourceId = iconResId;
+ mIconResId = iconResId;
+ mIconResName = iconResName;
mBitmapPath = bitmapPath;
}
}
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index 16486e1..36ab804 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -17,8 +17,9 @@
import android.annotation.NonNull;
import android.annotation.TestApi;
+import android.annotation.UserIdInt;
+import android.app.usage.UsageStatsManager;
import android.content.Context;
-import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -29,14 +30,16 @@
// TODO Enhance javadoc
/**
+ * <b>TODO: Update to reflect DR changes, such as manifest shortcuts.</b><br>
+ *
* {@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 #addDynamicShortcuts(List)}. There can be at most
- * {@link #getMaxDynamicShortcutCount()} number of dynamic shortcuts at a time from the same
- * application.
+ * {@link #getMaxShortcutCountForActivity()} number of dynamic shortcuts at a time from the
+ * same application.
* A dynamic shortcut can be deleted with {@link #removeDynamicShortcuts(List)}, and apps
* can also use {@link #removeAllDynamicShortcuts()} to delete all dynamic shortcuts.
*
@@ -50,7 +53,8 @@
* <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
+ * application can still always publish {@link #getMaxShortcutCountForActivity()} number of
+ * dynamic
* shortcuts.
*
* <h3>Shortcut IDs</h3>
@@ -80,16 +84,24 @@
*
* <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.
+ * Pinned shortcuts will be backed up and restored across devices. This means all information
+ * within shortcuts, including IDs, must be meaningful on different devices.
+ *
+ * <p>Note that:
+ * <ul>
+ * <li>Dynamic shortcuts will not be backed up or restored.
+ * <li>Icons of pinned shortcuts will <b>not</b> be backed up for performance reasons, unless
+ * they refer to resources. Instead, launcher applications are supposed to back up and restore
+ * icons of pinned shortcuts by themselves, and thus from the user's point of view, pinned
+ * shortcuts will look to have icons restored.
+ * </ul>
+ *
*
* <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)}.
- *
- * @hide
*/
public class ShortcutManager {
private static final String TAG = "ShortcutManager";
@@ -123,7 +135,7 @@
* @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.
+ * {@link #getMaxShortcutCountForActivity()} shortcuts.
*/
public boolean setDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
try {
@@ -136,7 +148,7 @@
/**
* Return all dynamic shortcuts from the caller application. The number of result items
- * will not exceed the value returned by {@link #getMaxDynamicShortcutCount()}.
+ * will not exceed the value returned by {@link #getMaxShortcutCountForActivity()}.
*/
@NonNull
public List<ShortcutInfo> getDynamicShortcuts() {
@@ -149,8 +161,21 @@
}
/**
- * Publish a single dynamic shortcut. If there's already dynamic or pinned shortcuts with
- * the same ID, they will all be updated.
+ * TODO Javadoc
+ */
+ @NonNull
+ public List<ShortcutInfo> getManifestShortcuts() {
+ try {
+ return mService.getManifestShortcuts(mContext.getPackageName(), injectMyUserId())
+ .getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Publish list of dynamic shortcuts. If there's already dynamic or pinned shortcuts with
+ * the same IDs, they will all be updated.
*
* <p>This API will be rate-limited.
*
@@ -169,7 +194,7 @@
}
/**
- * Delete a single dynamic shortcut by ID.
+ * Delete dynamic shortcuts by ID.
*/
public void removeDynamicShortcuts(@NonNull List<String> shortcutIds) {
try {
@@ -221,9 +246,60 @@
}
/**
- * Return the max number of dynamic shortcuts that each application can have at a time.
+ * TODO Javadoc
*/
- public int getMaxDynamicShortcutCount() {
+ public void disableShortcuts(@NonNull List<String> shortcutIds) {
+ try {
+ mService.disableShortcuts(mContext.getPackageName(), shortcutIds,
+ /* disabledMessage =*/ null, /* disabledMessageResId =*/ 0,
+ injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * TODO Javadoc
+ */
+ public void disableShortcuts(@NonNull List<String> shortcutIds, int disabledMessageResId) {
+ try {
+ mService.disableShortcuts(mContext.getPackageName(), shortcutIds,
+ /* disabledMessage =*/ null, disabledMessageResId,
+ injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * TODO Javadoc
+ */
+ public void disableShortcuts(@NonNull List<String> shortcutIds, String disabledMessage) {
+ try {
+ mService.disableShortcuts(mContext.getPackageName(), shortcutIds,
+ disabledMessage, /* disabledMessageResId =*/ 0,
+ injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * TODO Javadoc
+ */
+ public void enableShortcuts(@NonNull List<String> shortcutIds) {
+ try {
+ mService.enableShortcuts(mContext.getPackageName(), shortcutIds, injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the max number of dynamic shortcuts + manifest shortcuts that each launcehr icon
+ * can have at a time.
+ */
+ public int getMaxShortcutCountForActivity() {
try {
return mService.getMaxDynamicShortcutCount(mContext.getPackageName(), injectMyUserId());
} catch (RemoteException e) {
@@ -260,16 +336,66 @@
}
/**
- * Return the max width and height for icons, in pixels.
+ * Return the max width for icons, in pixels.
*/
- public int getIconMaxDimensions() {
+ public int getIconMaxWidth() {
try {
+ // TODO Implement it properly using xdpi.
return mService.getIconMaxDimensions(mContext.getPackageName(), injectMyUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+ /**
+ * Return the max height for icons, in pixels.
+ */
+ public int getIconMaxHeight() {
+ try {
+ // TODO Implement it properly using ydpi.
+ return mService.getIconMaxDimensions(mContext.getPackageName(), injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Applications that publish shortcuts should call this method whenever an action that's
+ * equivalent to an existing shortcut has been taken by the user. This includes not only when
+ * the user manually taps a shortcut, but when the user takes an equivalent action within the
+ * application -- for example, when a music player application has a shortcut to playlist X,
+ * then the application should not only report it when the playlist is opened from the
+ * shortcut, but also when the user plays the playlist within the application.
+ *
+ * <p>The information is accessible via {@link UsageStatsManager#queryEvents}
+ * Typically, launcher applications use this information to build a prediction model
+ * so that they can promote the shortcuts that are likely to be used at the moment.
+ */
+ public void reportShortcutUsed(String shortcutId) {
+ try {
+ mService.reportShortcutUsed(mContext.getPackageName(), shortcutId,
+ injectMyUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called internally when an application is considered to have come to foreground
+ * even when technically it's not. This method resets the throttling for this package.
+ * For example, when the user sends an "inline reply" on an notification, the system UI will
+ * call it.
+ *
+ * @hide
+ */
+ public void onApplicationActive(@NonNull String packageName, @UserIdInt int userId) {
+ try {
+ mService.onApplicationActive(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** @hide injection point */
@VisibleForTesting
protected int injectMyUserId() {
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index dd3a36c..6cd8403 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -28,8 +28,8 @@
*/
public class UserInfo implements Parcelable {
- /** 8 bits for user type */
- public static final int FLAG_MASK_USER_TYPE = 0x000000FF;
+ /** 16 bits for user type */
+ public static final int FLAG_MASK_USER_TYPE = 0x0000FFFF;
/**
* *************************** NOTE ***************************
@@ -87,6 +87,11 @@
*/
public static final int FLAG_EPHEMERAL = 0x00000100;
+ /**
+ * User is for demo purposes only and can be removed at any time.
+ */
+ public static final int FLAG_DEMO = 0x00000200;
+
public static final int NO_PROFILE_GROUP_ID = UserHandle.USER_NULL;
public int id;
@@ -153,6 +158,10 @@
return (flags & FLAG_INITIALIZED) == FLAG_INITIALIZED;
}
+ public boolean isDemo() {
+ return (flags & FLAG_DEMO) == FLAG_DEMO;
+ }
+
/**
* Returns true if the user is a split system user.
* <p>If {@link UserManager#isSplitSystemUser split system user mode} is not enabled,
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 6f43d99..b2d518c 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -1543,27 +1543,41 @@
* @hide
*/
public static String localesToResourceQualifier(LocaleList locs) {
- StringBuilder sb = new StringBuilder();
+ final StringBuilder sb = new StringBuilder();
for (int i = 0; i < locs.size(); i++) {
- Locale loc = locs.get(i);
- boolean l = (loc.getLanguage().length() != 0);
- boolean c = (loc.getCountry().length() != 0);
- boolean s = (loc.getScript().length() != 0);
- boolean v = (loc.getVariant().length() != 0);
- // TODO: take script and extensions into account
- if (l) {
- if (sb.length() != 0) {
- sb.append(",");
- }
+ final Locale loc = locs.get(i);
+ final int l = loc.getLanguage().length();
+ if (l == 0) {
+ continue;
+ }
+ final int s = loc.getScript().length();
+ final int c = loc.getCountry().length();
+ final int v = loc.getVariant().length();
+ // We ignore locale extensions, since they are not supported by AAPT
+
+ if (sb.length() != 0) {
+ sb.append(",");
+ }
+ if (l == 2 && s == 0 && (c == 0 || c == 2) && v == 0) {
+ // Traditional locale format: xx or xx-rYY
sb.append(loc.getLanguage());
- if (c) {
+ if (c == 2) {
sb.append("-r").append(loc.getCountry());
- if (s) {
- sb.append("-s").append(loc.getScript());
- if (v) {
- sb.append("-v").append(loc.getVariant());
- }
- }
+ }
+ } else {
+ sb.append("b+");
+ sb.append(loc.getLanguage());
+ if (s != 0) {
+ sb.append("+");
+ sb.append(loc.getScript());
+ }
+ if (c != 0) {
+ sb.append("+");
+ sb.append(loc.getCountry());
+ }
+ if (v != 0) {
+ sb.append("+");
+ sb.append(loc.getVariant());
}
}
}
diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java
index 38279a4..4b21187 100644
--- a/core/java/android/hardware/camera2/CameraCaptureSession.java
+++ b/core/java/android/hardware/camera2/CameraCaptureSession.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.hardware.camera2.params.OutputConfiguration;
import android.os.Handler;
import android.view.Surface;
@@ -220,6 +221,53 @@
public abstract void tearDown(@NonNull Surface surface) throws CameraAccessException;
/**
+ * <p>
+ * Finish the deferred output configurations where the output Surface was not configured before.
+ * </p>
+ * <p>
+ * For camera use cases where a preview and other output configurations need to be configured,
+ * it can take some time for the preview Surface to be ready (e.g., if the preview Surface is
+ * obtained from {@link android.view.SurfaceView}, the SurfaceView is ready after the UI layout
+ * is done, then it takes some time to get the preview Surface).
+ * </p>
+ * <p>
+ * To speed up camera startup time, the application can configure the
+ * {@link CameraCaptureSession} with the desired preview size, and defer the preview output
+ * configuration until the Surface is ready. After the {@link CameraCaptureSession} is created
+ * successfully with this deferred configuration and other normal configurations, the
+ * application can submit requests that don't include deferred output Surfaces. Once the
+ * deferred Surface is ready, the application can set the Surface to the same deferred output
+ * configuration with the {@link OutputConfiguration#setDeferredSurface} method, and then finish
+ * the deferred output configuration via this method, before it can submit requests with this
+ * output target.
+ * </p>
+ * <p>
+ * The output Surfaces included by this list of deferred {@link OutputConfiguration
+ * OutputConfigurations} can be used as {@link CaptureRequest} targets as soon as this call
+ * returns;
+ * </p>
+ * <p>
+ * This method is not supported by Legacy devices.
+ * </p>
+ *
+ * @param deferredOutputConfigs a list of {@link OutputConfiguration OutputConfigurations} that
+ * have had {@link OutputConfiguration#setDeferredSurface setDeferredSurface} invoked
+ * with a valid output Surface.
+ * @throws CameraAccessException if the camera device is no longer connected or has encountered
+ * a fatal error.
+ * @throws IllegalStateException if this session is no longer active, either because the session
+ * was explicitly closed, a new session has been created or the camera device has
+ * been closed. Or if this output configuration was already finished with the
+ * included surface before.
+ * @throws IllegalArgumentException for invalid output configurations, including ones where the
+ * source of the Surface is no longer valid or the Surface is from a unsupported
+ * source.
+ * @hide
+ */
+ public abstract void finishDeferredConfiguration(
+ List<OutputConfiguration> deferredOutputConfigs) throws CameraAccessException;
+
+ /**
* <p>Submit a request for an image to be captured by the camera device.</p>
*
* <p>The request defines all the parameters for capturing the single image,
diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
index 6736d34..c23bd5b 100644
--- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -24,6 +24,7 @@
import android.hardware.camera2.dispatch.DuckTypingDispatcher;
import android.hardware.camera2.dispatch.HandlerDispatcher;
import android.hardware.camera2.dispatch.InvokeDispatcher;
+import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.TaskDrainer;
import android.hardware.camera2.utils.TaskSingleDrainer;
import android.os.Handler;
@@ -156,6 +157,12 @@
}
@Override
+ public void finishDeferredConfiguration(
+ List<OutputConfiguration> deferredOutputConfigs) throws CameraAccessException {
+ mDeviceImpl.finishDeferredConfig(deferredOutputConfigs);
+ }
+
+ @Override
public synchronized int capture(CaptureRequest request, CaptureCallback callback,
Handler handler) throws CameraAccessException {
if (request == null) {
diff --git a/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
index 8cd1da5..1c8e124 100644
--- a/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
@@ -21,6 +21,7 @@
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.utils.SurfaceUtils;
import android.os.Handler;
@@ -256,6 +257,12 @@
return mSessionImpl.isAborting();
}
+ @Override
+ public void finishDeferredConfiguration(List<OutputConfiguration> deferredOutputConfigs)
+ throws CameraAccessException {
+ mSessionImpl.finishDeferredConfiguration(deferredOutputConfigs);
+ }
+
private class WrapperCallback extends StateCallback {
private final StateCallback mCallback;
@@ -263,26 +270,32 @@
mCallback = callback;
}
+ @Override
public void onConfigured(CameraCaptureSession session) {
mCallback.onConfigured(CameraConstrainedHighSpeedCaptureSessionImpl.this);
}
+ @Override
public void onConfigureFailed(CameraCaptureSession session) {
mCallback.onConfigureFailed(CameraConstrainedHighSpeedCaptureSessionImpl.this);
}
+ @Override
public void onReady(CameraCaptureSession session) {
mCallback.onReady(CameraConstrainedHighSpeedCaptureSessionImpl.this);
}
+ @Override
public void onActive(CameraCaptureSession session) {
mCallback.onActive(CameraConstrainedHighSpeedCaptureSessionImpl.this);
}
+ @Override
public void onClosed(CameraCaptureSession session) {
mCallback.onClosed(CameraConstrainedHighSpeedCaptureSessionImpl.this);
}
+ @Override
public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
mCallback.onSurfacePrepared(CameraConstrainedHighSpeedCaptureSessionImpl.this,
surface);
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 0cee114..ee8a6d7 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -407,7 +407,10 @@
int streamId = mConfiguredOutputs.keyAt(i);
OutputConfiguration outConfig = mConfiguredOutputs.valueAt(i);
- if (!outputs.contains(outConfig)) {
+ if (!outputs.contains(outConfig) || outConfig.isDeferredConfiguration()) {
+ // Always delete the deferred output configuration when the session
+ // is created, as the deferred output configuration doesn't have unique surface
+ // related identifies.
deleteList.add(streamId);
} else {
addSet.remove(outConfig); // Don't create a stream previously created
@@ -744,6 +747,37 @@
}
}
+ public void finishDeferredConfig(List<OutputConfiguration> deferredConfigs)
+ throws CameraAccessException {
+ if (deferredConfigs == null || deferredConfigs.size() == 0) {
+ throw new IllegalArgumentException("deferred config is null or empty");
+ }
+
+ synchronized(mInterfaceLock) {
+ for (OutputConfiguration config : deferredConfigs) {
+ int streamId = -1;
+ for (int i = 0; i < mConfiguredOutputs.size(); i++) {
+ // Have to use equal here, as createCaptureSessionByOutputConfigurations() and
+ // createReprocessableCaptureSessionByConfigurations() do a copy of the configs.
+ if (config.equals(mConfiguredOutputs.valueAt(i))) {
+ streamId = mConfiguredOutputs.keyAt(i);
+ break;
+ }
+ }
+ if (streamId == -1) {
+ throw new IllegalArgumentException("Deferred config is not part of this "
+ + "session");
+ }
+
+ if (config.getSurface() == null) {
+ throw new IllegalArgumentException("The deferred config for stream " + streamId
+ + " must have a non-null surface");
+ }
+ mRemoteDevice.setDeferredConfiguration(streamId, config);
+ }
+ }
+ }
+
public int capture(CaptureRequest request, CaptureCallback callback, Handler handler)
throws CameraAccessException {
if (DEBUG) {
@@ -2005,6 +2039,7 @@
*
* <p> Handle binder death for ICameraDeviceUser. Trigger onError.</p>
*/
+ @Override
public void binderDied() {
Log.w(TAG, "CameraDevice " + mCameraId + " died unexpectedly");
diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
index ef5f6d7..d77f60b 100644
--- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
+++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
@@ -215,5 +215,14 @@
}
}
+ public void setDeferredConfiguration(int streamId, OutputConfiguration deferredConfig)
+ throws CameraAccessException {
+ try {
+ mRemoteDevice.setDeferredConfiguration(streamId, deferredConfig);
+ } catch (Throwable t) {
+ CameraManager.throwAsPublicException(t);
+ throw new UnsupportedOperationException("Unexpected exception", t);
+ }
+ }
}
diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java
index acbf214..b9e75ee 100644
--- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java
+++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java
@@ -567,6 +567,13 @@
}
@Override
+ public void setDeferredConfiguration(int steamId, OutputConfiguration config) {
+ String err = "Set deferred configuration is not supported on legacy devices";
+ Log.e(TAG, err);
+ throw new ServiceSpecificException(ICameraService.ERROR_INVALID_OPERATION, err);
+ }
+
+ @Override
public int createInputStream(int width, int height, int format) {
String err = "Creating input stream is not supported on legacy devices";
Log.e(TAG, err);
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 61b534b..69c00e9 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -20,6 +20,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.utils.HashCodeHelpers;
import android.hardware.camera2.utils.SurfaceUtils;
@@ -95,6 +97,21 @@
}
/**
+ * Unknown surface source type.
+ */
+ private final int SURFACE_TYPE_UNKNOWN = -1;
+
+ /**
+ * The surface is obtained from {@link android.view.SurfaceView}.
+ */
+ private final int SURFACE_TYPE_SURFACE_VIEW = 0;
+
+ /**
+ * The surface is obtained from {@link android.graphics.SurfaceTexture}.
+ */
+ private final int SURFACE_TYPE_SURFACE_TEXTURE = 1;
+
+ /**
* Create a new {@link OutputConfiguration} instance with a {@link Surface},
* with a surface group ID.
*
@@ -179,12 +196,110 @@
checkNotNull(surface, "Surface must not be null");
checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
mSurfaceGroupId = surfaceGroupId;
+ mSurfaceType = SURFACE_TYPE_UNKNOWN;
mSurface = surface;
mRotation = rotation;
mConfiguredSize = SurfaceUtils.getSurfaceSize(surface);
mConfiguredFormat = SurfaceUtils.getSurfaceFormat(surface);
mConfiguredDataspace = SurfaceUtils.getSurfaceDataspace(surface);
mConfiguredGenerationId = surface.getGenerationId();
+ mIsDeferredConfig = false;
+ }
+
+ /**
+ * Create a new {@link OutputConfiguration} instance, with desired Surface size and Surface
+ * source class.
+ * <p>
+ * This constructor takes an argument for desired Surface size and the Surface source class
+ * without providing the actual output Surface. This is used to setup a output configuration
+ * with a deferred Surface. The application can use this output configuration to create a
+ * session.
+ * </p>
+ * <p>
+ * However, the actual output Surface must be set via {@link #setDeferredSurface} and finish the
+ * deferred Surface configuration via {@link CameraCaptureSession#finishDeferredConfiguration}
+ * before submitting a request with this Surface target. The deferred Surface can only be
+ * obtained from either from {@link android.view.SurfaceView} by calling
+ * {@link android.view.SurfaceHolder#getSurface}, or from
+ * {@link android.graphics.SurfaceTexture} via
+ * {@link android.view.Surface#Surface(android.graphics.SurfaceTexture)}).
+ * </p>
+ *
+ * @param surfaceSize Size for the deferred surface.
+ * @param klass a non-{@code null} {@link Class} object reference that indicates the source of
+ * this surface. Only {@link android.view.SurfaceHolder SurfaceHolder.class} and
+ * {@link android.graphics.SurfaceTexture SurfaceTexture.class} are supported.
+ * @hide
+ */
+ public <T> OutputConfiguration(@NonNull Size surfaceSize, @NonNull Class<T> klass) {
+ checkNotNull(klass, "surfaceSize must not be null");
+ checkNotNull(klass, "klass must not be null");
+ if (klass == android.view.SurfaceHolder.class) {
+ mSurfaceType = SURFACE_TYPE_SURFACE_VIEW;
+ } else if (klass == android.graphics.SurfaceTexture.class) {
+ mSurfaceType = SURFACE_TYPE_SURFACE_TEXTURE;
+ } else {
+ mSurfaceType = SURFACE_TYPE_UNKNOWN;
+ throw new IllegalArgumentException("Unknow surface source class type");
+ }
+
+ mSurfaceGroupId = SURFACE_GROUP_ID_NONE;
+ mSurface = null;
+ mRotation = ROTATION_0;
+ mConfiguredSize = surfaceSize;
+ mConfiguredFormat = StreamConfigurationMap.imageFormatToInternal(ImageFormat.PRIVATE);
+ mConfiguredDataspace = StreamConfigurationMap.imageFormatToDataspace(ImageFormat.PRIVATE);
+ mConfiguredGenerationId = 0;
+ mIsDeferredConfig = true;
+ }
+
+ /**
+ * Check if this configuration has deferred configuration.
+ *
+ * <p>This will return true if the output configuration was constructed with surface deferred.
+ * It will return true even after the deferred surface is set later.</p>
+ *
+ * @return true if this configuration has deferred surface.
+ * @hide
+ */
+ public boolean isDeferredConfiguration() {
+ return mIsDeferredConfig;
+ }
+
+ /**
+ * Set the deferred surface to this OutputConfiguration.
+ *
+ * <p>
+ * The deferred surface must be obtained from either from {@link android.view.SurfaceView} by
+ * calling {@link android.view.SurfaceHolder#getSurface}, or from
+ * {@link android.graphics.SurfaceTexture} via
+ * {@link android.view.Surface#Surface(android.graphics.SurfaceTexture)}). After the deferred
+ * surface is set, the application must finish the deferred surface configuration via
+ * {@link CameraCaptureSession#finishDeferredConfiguration} before submitting a request with
+ * this surface target.
+ * </p>
+ *
+ * @param surface The deferred surface to be set.
+ * @throws IllegalArgumentException if the Surface is invalid.
+ * @throws IllegalStateException if a Surface was already set to this deferred
+ * OutputConfiguration.
+ * @hide
+ */
+ public void setDeferredSurface(@NonNull Surface surface) {
+ checkNotNull(surface, "Surface must not be null");
+ if (mSurface != null) {
+ throw new IllegalStateException("Deferred surface is already set!");
+ }
+
+ // This will throw IAE is the surface was abandoned.
+ Size surfaceSize = SurfaceUtils.getSurfaceSize(surface);
+ if (!surfaceSize.equals(mConfiguredSize)) {
+ Log.w(TAG, "Deferred surface size " + surfaceSize +
+ " is different with pre-configured size " + mConfiguredSize +
+ ", the pre-configured size will be used.");
+ }
+
+ mSurface = surface;
}
/**
@@ -203,10 +318,12 @@
this.mSurface = other.mSurface;
this.mRotation = other.mRotation;
this.mSurfaceGroupId = other.mSurfaceGroupId;
+ this.mSurfaceType = other.mSurfaceType;
this.mConfiguredDataspace = other.mConfiguredDataspace;
this.mConfiguredFormat = other.mConfiguredFormat;
this.mConfiguredSize = other.mConfiguredSize;
this.mConfiguredGenerationId = other.mConfiguredGenerationId;
+ this.mIsDeferredConfig = other.mIsDeferredConfig;
}
/**
@@ -215,16 +332,30 @@
private OutputConfiguration(@NonNull Parcel source) {
int rotation = source.readInt();
int surfaceSetId = source.readInt();
+ int surfaceType = source.readInt();
+ int width = source.readInt();
+ int height = source.readInt();
Surface surface = Surface.CREATOR.createFromParcel(source);
- checkNotNull(surface, "Surface must not be null");
checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
mSurfaceGroupId = surfaceSetId;
mSurface = surface;
mRotation = rotation;
- mConfiguredSize = SurfaceUtils.getSurfaceSize(mSurface);
- mConfiguredFormat = SurfaceUtils.getSurfaceFormat(mSurface);
- mConfiguredDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
- mConfiguredGenerationId = mSurface.getGenerationId();
+ if (surface != null) {
+ mSurfaceType = SURFACE_TYPE_UNKNOWN;
+ mConfiguredSize = SurfaceUtils.getSurfaceSize(mSurface);
+ mConfiguredFormat = SurfaceUtils.getSurfaceFormat(mSurface);
+ mConfiguredDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
+ mConfiguredGenerationId = mSurface.getGenerationId();
+ mIsDeferredConfig = true;
+ } else {
+ mSurfaceType = surfaceType;
+ mConfiguredSize = new Size(width, height);
+ mConfiguredFormat = StreamConfigurationMap.imageFormatToInternal(ImageFormat.PRIVATE);
+ mConfiguredGenerationId = 0;
+ mConfiguredDataspace =
+ StreamConfigurationMap.imageFormatToDataspace(ImageFormat.PRIVATE);
+ mIsDeferredConfig = false;
+ }
}
/**
@@ -291,7 +422,12 @@
}
dest.writeInt(mRotation);
dest.writeInt(mSurfaceGroupId);
- mSurface.writeToParcel(dest, flags);
+ dest.writeInt(mSurfaceType);
+ dest.writeInt(mConfiguredSize.getWidth());
+ dest.writeInt(mConfiguredSize.getHeight());
+ if (mSurface != null) {
+ mSurface.writeToParcel(dest, flags);
+ }
}
/**
@@ -311,13 +447,20 @@
return true;
} else if (obj instanceof OutputConfiguration) {
final OutputConfiguration other = (OutputConfiguration) obj;
+ boolean iSSurfaceEqual = mSurface == other.mSurface &&
+ mConfiguredGenerationId == other.mConfiguredGenerationId ;
+ if (mIsDeferredConfig) {
+ Log.i(TAG, "deferred config has the same surface");
+ iSSurfaceEqual = true;
+ }
return mRotation == other.mRotation &&
- mSurface == other.mSurface &&
- mConfiguredGenerationId == other.mConfiguredGenerationId &&
+ iSSurfaceEqual&&
mConfiguredSize.equals(other.mConfiguredSize) &&
mConfiguredFormat == other.mConfiguredFormat &&
mConfiguredDataspace == other.mConfiguredDataspace &&
- mSurfaceGroupId == other.mSurfaceGroupId;
+ mSurfaceGroupId == other.mSurfaceGroupId &&
+ mSurfaceType == other.mSurfaceType &&
+ mIsDeferredConfig == other.mIsDeferredConfig;
}
return false;
}
@@ -327,15 +470,26 @@
*/
@Override
public int hashCode() {
+ // Need ensure that the hashcode remains unchanged after set a deferred surface. Otherwise
+ // The deferred output configuration will be lost in the camera streammap after the deferred
+ // surface is set.
+ if (mIsDeferredConfig) {
+ return HashCodeHelpers.hashCode(
+ mRotation, mConfiguredSize.hashCode(), mConfiguredFormat, mConfiguredDataspace,
+ mSurfaceGroupId, mSurfaceType);
+ }
+
return HashCodeHelpers.hashCode(
mRotation, mSurface.hashCode(), mConfiguredGenerationId,
mConfiguredSize.hashCode(), mConfiguredFormat, mConfiguredDataspace, mSurfaceGroupId);
}
private static final String TAG = "OutputConfiguration";
- private final Surface mSurface;
+ private Surface mSurface;
private final int mRotation;
- private int mSurfaceGroupId;
+ private final int mSurfaceGroupId;
+ // Surface source type, this is only used by the deferred surface configuration objects.
+ private final int mSurfaceType;
// The size, format, and dataspace of the surface when OutputConfiguration is created.
private final Size mConfiguredSize;
@@ -343,4 +497,6 @@
private final int mConfiguredDataspace;
// Surface generation ID to distinguish changes to Surface native internals
private final int mConfiguredGenerationId;
+ // Flag indicating if this config has deferred surface.
+ private final boolean mIsDeferredConfig;
}
diff --git a/core/java/android/hardware/input/InputManagerInternal.java b/core/java/android/hardware/input/InputManagerInternal.java
index 10fc8e6..01a5404 100644
--- a/core/java/android/hardware/input/InputManagerInternal.java
+++ b/core/java/android/hardware/input/InputManagerInternal.java
@@ -59,4 +59,9 @@
* @param deviceId The id of input device.
*/
public abstract void toggleCapsLock(int deviceId);
+
+ /**
+ * Set whether the input stack should deliver pulse gesture events when the device is asleep.
+ */
+ public abstract void setPulseGestureEnabled(boolean enabled);
}
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index f9a7d19..8f21b38 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -21,6 +21,7 @@
import android.app.PendingIntent;
import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -476,6 +477,26 @@
}
/**
+ * Grants permission to specified package for USB device without showing system dialog.
+ * Only system components can call this function, as it requires the MANAGE_USB permission.
+ * @param device to request permissions for
+ * @param packageName of package to grant permissions
+ *
+ * {@hide}
+ */
+ public void grantPermission(UsbDevice device, String packageName) {
+ try {
+ int uid = mContext.getPackageManager()
+ .getPackageUidAsUser(packageName, mContext.getUserId());
+ mService.grantDevicePermission(device, uid);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Package " + packageName + " not found.", e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns true if the specified USB function is currently enabled when in device mode.
* <p>
* USB functions represent interfaces which are published to the host to access
diff --git a/core/java/android/net/ConnectivityMetricsLogger.java b/core/java/android/net/ConnectivityMetricsLogger.java
index d8cdde9..7373495 100644
--- a/core/java/android/net/ConnectivityMetricsLogger.java
+++ b/core/java/android/net/ConnectivityMetricsLogger.java
@@ -33,28 +33,39 @@
// Component Tags
public static final int COMPONENT_TAG_CONNECTIVITY = 0;
- public static final int COMPONENT_TAG_BLUETOOTH = 1;
- public static final int COMPONENT_TAG_WIFI = 2;
- public static final int COMPONENT_TAG_TELECOM = 3;
- public static final int COMPONENT_TAG_TELEPHONY = 4;
-
- public static final int NUMBER_OF_COMPONENTS = 5;
+ public static final int COMPONENT_TAG_BLUETOOTH = 1;
+ public static final int COMPONENT_TAG_WIFI = 2;
+ public static final int COMPONENT_TAG_TELECOM = 3;
+ public static final int COMPONENT_TAG_TELEPHONY = 4;
+ public static final int NUMBER_OF_COMPONENTS = 5;
// Event Tag
public static final int TAG_SKIPPED_EVENTS = -1;
public static final String DATA_KEY_EVENTS_COUNT = "count";
- private IConnectivityMetricsLogger mService;
-
- private long mServiceUnblockedTimestampMillis = 0;
- private int mNumSkippedEvents = 0;
+ /** {@hide} */ protected final IConnectivityMetricsLogger mService;
+ /** {@hide} */ protected volatile long mServiceUnblockedTimestampMillis;
+ private int mNumSkippedEvents;
public ConnectivityMetricsLogger() {
mService = IConnectivityMetricsLogger.Stub.asInterface(ServiceManager.getService(
CONNECTIVITY_METRICS_LOGGER_SERVICE));
}
+ /**
+ * Log a ConnectivityMetricsEvent.
+ *
+ * This method keeps track of skipped events when MetricsLoggerService throttles input events.
+ * It skips logging when MetricsLoggerService is active. When throttling ends, it logs a
+ * meta-event containing the number of events dropped. It is not safe to call this method
+ * concurrently from different threads.
+ *
+ * @param timestamp is the epoch timestamp of the event in ms.
+ * @param componentTag is the COMPONENT_* constant the event belongs to.
+ * @param eventTag is an event type constant whose meaning is specific to the component tag.
+ * @param data is a Parcelable instance representing the event.
+ */
public void logEvent(long timestamp, int componentTag, int eventTag, Parcelable data) {
if (mService == null) {
if (DBG) {
@@ -104,7 +115,7 @@
}
}
} catch (RemoteException e) {
- Log.e(TAG, "Error logging event " + e.getMessage());
+ Log.e(TAG, "Error logging event", e);
}
}
@@ -121,8 +132,8 @@
public ConnectivityMetricsEvent[] getEvents(ConnectivityMetricsEvent.Reference reference) {
try {
return mService.getEvents(reference);
- } catch (RemoteException ex) {
- Log.e(TAG, "IConnectivityMetricsLogger.getEvents: " + ex);
+ } catch (RemoteException e) {
+ Log.e(TAG, "IConnectivityMetricsLogger.getEvents", e);
return null;
}
}
@@ -133,8 +144,8 @@
public boolean register(PendingIntent newEventsIntent) {
try {
return mService.register(newEventsIntent);
- } catch (RemoteException ex) {
- Log.e(TAG, "IConnectivityMetricsLogger.register: " + ex);
+ } catch (RemoteException e) {
+ Log.e(TAG, "IConnectivityMetricsLogger.register", e);
return false;
}
}
@@ -142,8 +153,8 @@
public boolean unregister(PendingIntent newEventsIntent) {
try {
mService.unregister(newEventsIntent);
- } catch (RemoteException ex) {
- Log.e(TAG, "IConnectivityMetricsLogger.unregister: " + ex);
+ } catch (RemoteException e) {
+ Log.e(TAG, "IConnectivityMetricsLogger.unregister", e);
return false;
}
diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java
index 5511a24..69f50a2 100644
--- a/core/java/android/net/NetworkMisc.java
+++ b/core/java/android/net/NetworkMisc.java
@@ -52,6 +52,15 @@
public boolean acceptUnvalidated;
/**
+ * Set to avoid surfacing the "Sign in to network" notification.
+ * if carrier receivers/apps are registered to handle the carrier-specific provisioning
+ * procedure, a carrier specific provisioning notification will be placed.
+ * only one notification should be displayed. This field is set based on
+ * which notification should be used for provisioning.
+ */
+ public boolean provisioningNotificationDisabled;
+
+ /**
* For mobile networks, this is the subscriber ID (such as IMSI).
*/
public String subscriberId;
@@ -65,6 +74,7 @@
explicitlySelected = nm.explicitlySelected;
acceptUnvalidated = nm.acceptUnvalidated;
subscriberId = nm.subscriberId;
+ provisioningNotificationDisabled = nm.provisioningNotificationDisabled;
}
}
@@ -79,6 +89,7 @@
out.writeInt(explicitlySelected ? 1 : 0);
out.writeInt(acceptUnvalidated ? 1 : 0);
out.writeString(subscriberId);
+ out.writeInt(provisioningNotificationDisabled ? 1 : 0);
}
public static final Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -89,6 +100,7 @@
networkMisc.explicitlySelected = in.readInt() != 0;
networkMisc.acceptUnvalidated = in.readInt() != 0;
networkMisc.subscriberId = in.readString();
+ networkMisc.provisioningNotificationDisabled = in.readInt() != 0;
return networkMisc;
}
diff --git a/core/java/android/net/metrics/IpConnectivityEvent.java b/core/java/android/net/metrics/IpConnectivityEvent.java
index 95576c2..d3771dc 100644
--- a/core/java/android/net/metrics/IpConnectivityEvent.java
+++ b/core/java/android/net/metrics/IpConnectivityEvent.java
@@ -16,20 +16,13 @@
package android.net.metrics;
-import android.net.ConnectivityMetricsLogger;
-import android.os.Parcel;
import android.os.Parcelable;
-/**
- * {@hide}
- */
+/** {@hide} */
public abstract class IpConnectivityEvent {
- private static final int COMPONENT_TAG = ConnectivityMetricsLogger.COMPONENT_TAG_CONNECTIVITY;
+ private static final IpConnectivityLog sMetricsLog = new IpConnectivityLog();
- private static final ConnectivityMetricsLogger sMetricsLogger = new ConnectivityMetricsLogger();
-
- public static <T extends IpConnectivityEvent & Parcelable> void logEvent(T event) {
- // TODO: consider using different component for DNS event.
- sMetricsLogger.logEvent(System.currentTimeMillis(), COMPONENT_TAG, 0, event);
+ static <T extends IpConnectivityEvent & Parcelable> void logEvent(T event) {
+ sMetricsLog.log(System.currentTimeMillis(), event);
}
-};
+}
diff --git a/core/java/android/net/metrics/IpConnectivityLog.java b/core/java/android/net/metrics/IpConnectivityLog.java
new file mode 100644
index 0000000..233ff74
--- /dev/null
+++ b/core/java/android/net/metrics/IpConnectivityLog.java
@@ -0,0 +1,70 @@
+/*
+ * 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.net.metrics;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.ConnectivityMetricsLogger;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Specialization of the ConnectivityMetricsLogger class for recording IP connectivity events.
+ * {@hide}
+ */
+class IpConnectivityLog extends ConnectivityMetricsLogger {
+ private static String TAG = "IpConnectivityMetricsLogger";
+ private static final boolean DBG = false;
+
+ /**
+ * Log an IpConnectivity event. Contrary to logEvent(), this method does not
+ * keep track of skipped events and is thread-safe for callers.
+ *
+ * @param timestamp is the epoch timestamp of the event in ms.
+ * @param data is a Parcelable IpConnectivityEvent instance representing the event.
+ *
+ * @return true if the event was successfully logged.
+ */
+ public <T extends IpConnectivityEvent & Parcelable> boolean log(long timestamp, T data) {
+ if (mService == null) {
+ if (DBG) {
+ Log.d(TAG, CONNECTIVITY_METRICS_LOGGER_SERVICE + " service not ready");
+ }
+ return false;
+ }
+
+ if (System.currentTimeMillis() < mServiceUnblockedTimestampMillis) {
+ if (DBG) {
+ Log.d(TAG, "skipping logging due to throttling for IpConnectivity component");
+ }
+ return false;
+ }
+
+ try {
+ final ConnectivityMetricsEvent event =
+ new ConnectivityMetricsEvent(timestamp, COMPONENT_TAG_CONNECTIVITY, 0, data);
+ final long result = mService.logEvent(event);
+ if (result >= 0) {
+ mServiceUnblockedTimestampMillis = result;
+ }
+ return (result == 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error logging event", e);
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 41ff9fd..783c25a 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -290,6 +290,7 @@
// Guarded by NfcAdapter.class
static boolean sIsInitialized = false;
+ static boolean sHasNfcFeature;
// Final after first constructor, except for
// attemptDeadServiceRecovery() when NFC crashes - we accept a best effort
@@ -435,42 +436,65 @@
}
/**
+ * Helper to check if this device is NFC HCE capable, by checking for
+ * FEATURE_NFC_HOST_CARD_EMULATION and/or FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
+ * but without using a context.
+ */
+ private static boolean hasNfcHceFeature() {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm == null) {
+ Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
+ return false;
+ }
+ try {
+ return pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)
+ || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
+ return false;
+ }
+ }
+
+ /**
* Returns the NfcAdapter for application context,
* or throws if NFC is not available.
* @hide
*/
public static synchronized NfcAdapter getNfcAdapter(Context context) {
if (!sIsInitialized) {
+ sHasNfcFeature = hasNfcFeature();
+ boolean hasHceFeature = hasNfcHceFeature();
/* is this device meant to have NFC */
- if (!hasNfcFeature()) {
+ if (!sHasNfcFeature && !hasHceFeature) {
Log.v(TAG, "this device does not have NFC support");
throw new UnsupportedOperationException();
}
-
sService = getServiceInterface();
if (sService == null) {
Log.e(TAG, "could not retrieve NFC service");
throw new UnsupportedOperationException();
}
- try {
- sTagService = sService.getNfcTagInterface();
- } catch (RemoteException e) {
- Log.e(TAG, "could not retrieve NFC Tag service");
- throw new UnsupportedOperationException();
+ if (sHasNfcFeature) {
+ try {
+ sTagService = sService.getNfcTagInterface();
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not retrieve NFC Tag service");
+ throw new UnsupportedOperationException();
+ }
}
-
- try {
- sCardEmulationService = sService.getNfcCardEmulationInterface();
- } catch (RemoteException e) {
- Log.e(TAG, "could not retrieve card emulation service");
- throw new UnsupportedOperationException();
- }
-
- try {
- sNfcFCardEmulationService = sService.getNfcFCardEmulationInterface();
- } catch (RemoteException e) {
- Log.e(TAG, "could not retrieve NFC-F card emulation service");
- throw new UnsupportedOperationException();
+ if (hasHceFeature) {
+ try {
+ sNfcFCardEmulationService = sService.getNfcFCardEmulationInterface();
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not retrieve NFC-F card emulation service");
+ throw new UnsupportedOperationException();
+ }
+ try {
+ sCardEmulationService = sService.getNfcCardEmulationInterface();
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not retrieve card emulation service");
+ throw new UnsupportedOperationException();
+ }
}
sIsInitialized = true;
@@ -838,8 +862,14 @@
*
* @param uris an array of Uri(s) to push over Android Beam
* @param activity activity for which the Uri(s) will be pushed
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void setBeamPushUris(Uri[] uris, Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null) {
throw new NullPointerException("activity cannot be null");
}
@@ -914,8 +944,14 @@
*
* @param callback callback, or null to disable
* @param activity activity for which the Uri(s) will be pushed
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null) {
throw new NullPointerException("activity cannot be null");
}
@@ -992,9 +1028,15 @@
* @param activities optional additional activities, however we strongly recommend
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void setNdefPushMessage(NdefMessage message, Activity activity,
Activity ... activities) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
int targetSdkVersion = getSdkVersion();
try {
if (activity == null) {
@@ -1024,6 +1066,11 @@
*/
@SystemApi
public void setNdefPushMessage(NdefMessage message, Activity activity, int flags) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null) {
throw new NullPointerException("activity cannot be null");
}
@@ -1094,9 +1141,15 @@
* @param activities optional additional activities, however we strongly recommend
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity,
Activity ... activities) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
int targetSdkVersion = getSdkVersion();
try {
if (activity == null) {
@@ -1168,9 +1221,15 @@
* @param activities optional additional activities, however we strongly recommend
* to only register one at a time, and to do so in that activity's
* {@link Activity#onCreate}
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback callback,
Activity activity, Activity ... activities) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
int targetSdkVersion = getSdkVersion();
try {
if (activity == null) {
@@ -1227,9 +1286,15 @@
* @param techLists the tech lists used to perform matching for dispatching of the
* {@link NfcAdapter#ACTION_TECH_DISCOVERED} intent
* @throws IllegalStateException if the Activity is not currently in the foreground
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void enableForegroundDispatch(Activity activity, PendingIntent intent,
IntentFilter[] filters, String[][] techLists) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null || intent == null) {
throw new NullPointerException();
}
@@ -1263,8 +1328,14 @@
*
* @param activity the Activity to disable dispatch to
* @throws IllegalStateException if the Activity has already been paused
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void disableForegroundDispatch(Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
ActivityThread.currentActivityThread().unregisterOnActivityPausedListener(activity,
mForegroundDispatchListener);
disableForegroundDispatchInternal(activity, false);
@@ -1309,9 +1380,15 @@
* @param callback the callback to be called when a tag is discovered
* @param flags Flags indicating poll technologies and other optional parameters
* @param extras Additional extras for configuring reader mode.
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void enableReaderMode(Activity activity, ReaderCallback callback, int flags,
Bundle extras) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
mNfcActivityManager.enableReaderMode(activity, callback, flags, extras);
}
@@ -1321,8 +1398,14 @@
* all supported tag technologies.
*
* @param activity the Activity that currently has reader mode enabled
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public void disableReaderMode(Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
mNfcActivityManager.disableReaderMode(activity);
}
@@ -1349,8 +1432,14 @@
*
* @param activity the current foreground Activity that has registered data to share
* @return whether the Beam animation was successfully invoked
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public boolean invokeBeam(Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null) {
throw new NullPointerException("activity may not be null.");
}
@@ -1404,10 +1493,16 @@
* @param activity foreground activity
* @param message a NDEF Message to push over NFC
* @throws IllegalStateException if the activity is not currently in the foreground
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
* @deprecated use {@link #setNdefPushMessage} instead
*/
@Deprecated
public void enableForegroundNdefPush(Activity activity, NdefMessage message) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null || message == null) {
throw new NullPointerException();
}
@@ -1432,10 +1527,16 @@
*
* @param activity the Foreground activity
* @throws IllegalStateException if the Activity has already been paused
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
* @deprecated use {@link #setNdefPushMessage} instead
*/
@Deprecated
public void disableForegroundNdefPush(Activity activity) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
if (activity == null) {
throw new NullPointerException();
}
@@ -1452,6 +1553,9 @@
*/
@SystemApi
public boolean enableNdefPush() {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
try {
return sService.enableNdefPush();
} catch (RemoteException e) {
@@ -1467,6 +1571,11 @@
*/
@SystemApi
public boolean disableNdefPush() {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
try {
return sService.disableNdefPush();
} catch (RemoteException e) {
@@ -1497,8 +1606,14 @@
*
* @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS
* @return true if NDEF Push feature is enabled
+ * @throws UnsupportedOperationException if FEATURE_NFC is unavailable.
*/
public boolean isNdefPushEnabled() {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
try {
return sService.isNdefPushEnabled();
} catch (RemoteException e) {
@@ -1623,6 +1738,11 @@
@SystemApi
public boolean addNfcUnlockHandler(final NfcUnlockHandler unlockHandler,
String[] tagTechnologies) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
// If there are no tag technologies, don't bother adding unlock handler
if (tagTechnologies.length == 0) {
return false;
@@ -1666,6 +1786,11 @@
*/
@SystemApi
public boolean removeNfcUnlockHandler(NfcUnlockHandler unlockHandler) {
+ synchronized (NfcAdapter.class) {
+ if (!sHasNfcFeature) {
+ throw new UnsupportedOperationException();
+ }
+ }
try {
synchronized (mLock) {
if (mNfcUnlockHandlers.containsKey(unlockHandler)) {
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index d8be2b6..80927f3 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -340,6 +340,33 @@
}
/**
+ * Return preloads directory.
+ * <p>This directory may contain pre-loaded content such as
+ * {@link #getDataPreloadsDemoDirectory() demo videos} and
+ * {@link #getDataPreloadsAppsDirectory() APK files} .
+ * {@hide}
+ */
+ public static File getDataPreloadsDirectory() {
+ return new File(getDataDirectory(), "preloads");
+ }
+
+ /**
+ * @see #getDataPreloadsDirectory()
+ * {@hide}
+ */
+ public static File getDataPreloadsDemoDirectory() {
+ return new File(getDataPreloadsDirectory(), "demo");
+ }
+
+ /**
+ * @see #getDataPreloadsDirectory()
+ * {@hide}
+ */
+ public static File getDataPreloadsAppsDirectory() {
+ return new File(getDataPreloadsDirectory(), "apps");
+ }
+
+ /**
* Return the primary shared/external storage directory. This directory may
* not currently be accessible if it has been mounted by the user on their
* computer, has been removed from the device, or some other problem has
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index b3cf710..9bbe8f9 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -57,14 +57,19 @@
/**
* Power hint:
* Interaction: The user is interacting with the device. The corresponding data field must be
- * the expected duration of the fling, or 0 if unknown.
+ * the expected duration of the interaction, or 0 if unknown.
*
- * Sustained Performance Mode: Enable/Disables Sustained Performance Mode.
+ * Sustained Performance Mode: The corresponding data field must be Enable/Disable
+ * Sustained Performance Mode.
+ *
+ * Launch: This is specific for activity launching. The corresponding data field must be
+ * the expected duration of the required boost, or 0 if unknown.
*
* These must be kept in sync with the values in hardware/libhardware/include/hardware/power.h
*/
public static final int POWER_HINT_INTERACTION = 2;
public static final int POWER_HINT_SUSTAINED_PERFORMANCE_MODE = 6;
+ public static final int POWER_HINT_LAUNCH = 8;
public static String wakefulnessToString(int wakefulness) {
switch (wakefulness) {
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index dd7be53..507379b 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -570,18 +570,19 @@
* @throws SecurityException if the current user is not allowed to wipe data.
*/
public static void rebootWipeUserData(Context context) throws IOException {
- rebootWipeUserData(context, false, context.getPackageName());
+ rebootWipeUserData(context, false /* shutdown */, context.getPackageName(),
+ false /* force */);
}
/** {@hide} */
public static void rebootWipeUserData(Context context, String reason) throws IOException {
- rebootWipeUserData(context, false, reason);
+ rebootWipeUserData(context, false /* shutdown */, reason, false /* force */);
}
/** {@hide} */
public static void rebootWipeUserData(Context context, boolean shutdown)
throws IOException {
- rebootWipeUserData(context, shutdown, context.getPackageName());
+ rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */);
}
/**
@@ -595,6 +596,9 @@
* @param shutdown if true, the device will be powered down after
* the wipe completes, rather than being rebooted
* back to the regular system.
+ * @param reason the reason for the wipe that is visible in the logs
+ * @param force whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction
+ * should be ignored
*
* @throws IOException if writing the recovery command file
* fails, or if the reboot itself fails.
@@ -602,10 +606,10 @@
*
* @hide
*/
- public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
- throws IOException {
+ public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
+ boolean force) throws IOException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
- if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
+ if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
@@ -658,6 +662,31 @@
}
/**
+ * Reboot into recovery and wipe the A/B device.
+ *
+ * @param Context the Context to use.
+ * @param packageFile the wipe package to be applied.
+ * @param reason the reason to wipe.
+ *
+ * @throws IOException if something goes wrong.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void rebootWipeAb(Context context, File packageFile, String reason)
+ throws IOException {
+ String reasonArg = null;
+ if (!TextUtils.isEmpty(reason)) {
+ reasonArg = "--reason=" + sanitizeArg(reason);
+ }
+
+ final String filename = packageFile.getCanonicalPath();
+ final String filenameArg = "--wipe_package=" + filename;
+ final String localeArg = "--locale=" + Locale.getDefault().toString();
+ bootCommand(context, "--wipe_ab", filenameArg, reasonArg, localeArg);
+ }
+
+ /**
* Reboot into the recovery system with the supplied argument.
* @param args to pass to the recovery utility.
* @throws IOException if something goes wrong.
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index bcc8d46..d742206 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -789,7 +789,7 @@
*/
public boolean isPrimaryUser() {
UserInfo user = getUserInfo(UserHandle.myUserId());
- return user != null ? user.isPrimary() : false;
+ return user != null && user.isPrimary();
}
/**
@@ -855,7 +855,19 @@
*/
public boolean isGuestUser() {
UserInfo user = getUserInfo(UserHandle.myUserId());
- return user != null ? user.isGuest() : false;
+ return user != null && user.isGuest();
+ }
+
+ /**
+ * Checks if the calling app is running in a demo user.
+ * <p>
+ * Caller must hold the MANAGE_USERS permission.
+ * @return whether the caller is a demo user.
+ * @hide
+ */
+ public boolean isDemoUser() {
+ UserInfo user = getUserInfo(UserHandle.myUserId());
+ return user != null && user.isDemo();
}
/**
@@ -1975,6 +1987,10 @@
if (!supportsMultipleUsers()) {
return false;
}
+ // If Demo Mode is on, don't show user switcher
+ if (isDeviceInDemoMode(mContext)) {
+ return false;
+ }
List<UserInfo> users = getUsers(true);
if (users == null) {
return false;
@@ -1991,6 +2007,14 @@
}
/**
+ * @hide
+ */
+ public static boolean isDeviceInDemoMode(Context context) {
+ return Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
+ }
+
+ /**
* Returns a serial number on this device for a given userHandle. User handles can be recycled
* when deleting and creating users, but serial numbers are not reused until the device is wiped.
* @param userHandle
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 485bbd1..968996f 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -45,8 +45,11 @@
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
@@ -115,6 +118,10 @@
public static final int FLAG_INCLUDE_INVISIBLE = 1 << 10;
private static volatile IMountService sMountService = null;
+ private static final String INTERNAL_STORAGE_SIZE_PATH =
+ "/sys/block/mmcblk0/size";
+ private static final String INTERNAL_STORAGE_SECTOR_SIZE =
+ "/sys/block/mmcblk0/queue/hw_sector_size";
private final Context mContext;
private final ContentResolver mResolver;
@@ -903,6 +910,23 @@
return getVolumeList(UserHandle.myUserId(), FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE)[0];
}
+ /** {@hide} */
+ public long getPrimaryStorageSize() {
+ final long numberBlocks = readLong(INTERNAL_STORAGE_SIZE_PATH);
+ final long sectorSize = readLong(INTERNAL_STORAGE_SECTOR_SIZE);
+ return numberBlocks * sectorSize;
+ }
+
+ private long readLong(String path) {
+ try (final FileInputStream fis = new FileInputStream(path);
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(fis));) {
+ return Long.parseLong(reader.readLine());
+ } catch (Exception e) {
+ Slog.w("Could not read " + path, e);
+ return 0;
+ }
+ }
+
/** @removed */
public @NonNull StorageVolume[] getVolumeList() {
return getVolumeList(mContext.getUserId(), 0);
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 893eb37..fbd61cf 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -179,6 +179,7 @@
* <li>{@link #VOICEMAIL_TYPE}</li>
* <li>{@link #REJECTED_TYPE}</li>
* <li>{@link #BLOCKED_TYPE}</li>
+ * <li>{@link #ANSWERED_EXTERNALLY_TYPE}</li>
* </ul>
* </p>
*/
@@ -200,7 +201,6 @@
* Call log type for a call which was answered on another device. Used in situations where
* a call rings on multiple devices simultaneously and it ended up being answered on a
* device other than the current one.
- * @hide
*/
public static final int ANSWERED_EXTERNALLY_TYPE = 7;
@@ -214,10 +214,7 @@
/** Call had video. */
public static final int FEATURES_VIDEO = 0x1;
- /**
- * Call was pulled externally.
- * @hide
- */
+ /** Call was pulled externally. */
public static final int FEATURES_PULLED_EXTERNALLY = 0x2;
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 6d2d7c0..991e2ac 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1277,6 +1277,17 @@
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_WEBVIEW_SETTINGS = "android.settings.WEBVIEW_SETTINGS";
+ /**
+ * Activity Action: Show the Deletion Helper settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_DELETION_HELPER_SETTINGS
+ = "android.settings.DELETION_HELPER_SETTINGS";
+
// End of Intent actions for Settings
/**
@@ -6236,6 +6247,29 @@
"managed_profile_contact_remote_search";
/**
+ * Whether or not the automatic storage manager is enabled and should run on the device.
+ *
+ * @hide
+ */
+ public static final String AUTOMATIC_STORAGE_MANAGER_ENABLED =
+ "automatic_storage_manager_enabled";
+
+ /**
+ * How many days of information for the automatic storage manager to retain on the device.
+ *
+ * @hide
+ */
+ public static final String AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN =
+ "automatic_storage_manager_days_to_retain";
+
+ /**
+ * Default number of days of information for the automatic storage manager to retain.
+ *
+ * @hide
+ */
+ public static final int AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_DEFAULT = 90;
+
+ /**
* This are the settings to be backed up.
*
* NOTE: Settings are backed up and restored in the order they appear
@@ -8558,6 +8592,24 @@
"ephemeral_cookie_max_size_bytes";
/**
+ * A mask applied to the ephemeral hash to generate the hash prefix.
+ * <p>
+ * Type: int
+ *
+ * @hide
+ */
+ public static final String EPHEMERAL_HASH_PREFIX_MASK = "ephemeral_hash_prefix_mask";
+
+ /**
+ * Number of hash prefixes to send during ephemeral resolution.
+ * <p>
+ * Type: int
+ *
+ * @hide
+ */
+ public static final String EPHEMERAL_HASH_PREFIX_COUNT = "ephemeral_hash_prefix_count";
+
+ /**
* The duration for caching uninstalled ephemeral apps.
* <p>
* Type: long
@@ -8593,6 +8645,15 @@
public static final String SAFE_BOOT_DISALLOWED = "safe_boot_disallowed";
/**
+ * Whether this device is currently in retail demo mode. If true, device
+ * usage is severely limited.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ * @hide
+ */
+ public static final String DEVICE_DEMO_MODE = "device_demo_mode";
+
+ /**
* Settings to backup. This is here so that it's in the same place as the settings
* keys and easy to update.
*
@@ -8991,12 +9052,28 @@
public static final String ENABLE_CELLULAR_ON_BOOT = "enable_cellular_on_boot";
/**
+ * Whether toggling OEM unlock is disallowed. If disallowed, it is not possible to enable or
+ * disable OEM unlock.
+ * <p>
+ * Type: int (0: allow OEM unlock setting. 1: disallow OEM unlock)
+ * @hide
+ */
+ public static final String OEM_UNLOCK_DISALLOWED = "oem_unlock_disallowed";
+
+ /**
* The maximum allowed notification enqueue rate in Hertz.
*
* Should be a float, and includes both posts and updates.
* @hide
*/
public static final String MAX_NOTIFICATION_ENQUEUE_RATE = "max_notification_enqueue_rate";
+
+ /**
+ * Whether SystemUI navigation keys is enabled.
+ * @hide
+ */
+ public static final String SYSTEM_NAVIGATION_KEYS_ENABLED =
+ "system_navigation_keys_enabled";
}
/**
diff --git a/core/java/android/provider/VoicemailContract.java b/core/java/android/provider/VoicemailContract.java
index 6a3cc02..ab9e497 100644
--- a/core/java/android/provider/VoicemailContract.java
+++ b/core/java/android/provider/VoicemailContract.java
@@ -25,7 +25,6 @@
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
-import android.database.Cursor;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.telecom.PhoneAccount;
@@ -107,6 +106,47 @@
public static final String ACTION_SYNC_VOICEMAIL = "android.provider.action.SYNC_VOICEMAIL";
/**
+ * Broadcast intent to inform a new visual voicemail SMS has been received. This intent will
+ * only be delivered to the voicemail client. The intent will have the following extra values:
+ *
+ * <ul>
+ * <li><em>{@link #EXTRA_VOICEMAIL_SMS_TYPE}</em> - (String) The event type of the SMS. Common
+ * values are "SYNC" or "STATUS"</li>
+ * <li><em>{@link #EXTRA_VOICEMAIL_SMS_DATA}</em> - (Bundle) The fields sent by the SMS</li>
+ * <li><em>{@link #EXTRA_VOICEMAIL_SMS_SUBID}</em> - (Integer) The subscription ID of the
+ * phone account that received the SMS</li>
+ * </ul>
+ */
+ /** @hide */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_VOICEMAIL_SMS_RECEIVED =
+ "android.intent.action.VOICEMAIL_SMS_RECEIVED";
+
+ /**
+ * Extra included in {@link #ACTION_VOICEMAIL_SMS_RECEIVED} broadcast intents to indicate the
+ * event type of the SMS. Common values are "SYNC" or "STATUS"
+ */
+ /** @hide */
+ public static final String EXTRA_VOICEMAIL_SMS_PREFIX =
+ "com.android.voicemail.extra.VOICEMAIL_SMS_PREFIX";
+
+ /**
+ * Extra included in {@link #ACTION_VOICEMAIL_SMS_RECEIVED} broadcast intents to indicate the
+ * fields sent by the SMS
+ */
+ /** @hide */
+ public static final String EXTRA_VOICEMAIL_SMS_FIELDS =
+ "com.android.voicemail.extra.VOICEMAIL_SMS_FIELDS";
+
+ /**
+ * Extra included in {@link #ACTION_VOICEMAIL_SMS_RECEIVED} broadcast intents to indicate he
+ * subscription ID of the phone account that received the SMS.
+ */
+ /** @hide */
+ public static final String EXTRA_VOICEMAIL_SMS_SUBID =
+ "com.android.voicemail.extra.VOICEMAIL_SMS_SUBID";
+
+ /**
* Extra included in {@link Intent#ACTION_PROVIDER_CHANGED} broadcast intents to indicate if the
* receiving package made this change.
*/
@@ -360,6 +400,20 @@
*/
public static final String SOURCE_PACKAGE = SOURCE_PACKAGE_FIELD;
+ /**
+ * The type of the source, which determines how to interpret source-specific states.
+ * Typically this will be set to the same string as
+ * {@link android.telephony.CarrierConfigManager#KEY_VVM_TYPE_STRING}. For example,
+ * "vvm_type_omtp".
+ *
+ * <P>Type: TEXT</P>
+ *
+ * @see #CONFIGURATION_STATE
+ * @see #DATA_CHANNEL_STATE
+ * @see #NOTIFICATION_CHANNEL_STATE
+ */
+ public static final String SOURCE_TYPE = "source_type";
+
// Note: Multiple entries may exist for a single source if they are differentiated by the
// PHONE_ACCOUNT_* fields.
@@ -392,6 +446,10 @@
public static final String VOICEMAIL_ACCESS_URI = "voicemail_access_uri";
/**
* The configuration state of the voicemail source.
+ *
+ * <P>Negative values are reserved to the source for source-specific states, see
+ * {@link #SOURCE_TYPE}
+ *
* <P> Possible values:
* {@link #CONFIGURATION_STATE_OK},
* {@link #CONFIGURATION_STATE_NOT_CONFIGURED},
@@ -399,14 +457,7 @@
* <P>Type: INTEGER</P>
*/
public static final String CONFIGURATION_STATE = "configuration_state";
- /**
- * Value of {@link #CONFIGURATION_STATE} passed into
- * {@link #setStatus(Context, PhoneAccountHandle, int, int, int)} to indicate that the
- * {@link #CONFIGURATION_STATE} field is not to be changed
- *
- * @hide
- */
- public static final int CONFIGURATION_STATE_IGNORE = -1;
+
/** Value of {@link #CONFIGURATION_STATE} to indicate an all OK configuration status. */
public static final int CONFIGURATION_STATE_OK = 0;
/**
@@ -424,6 +475,10 @@
/**
* The data channel state of the voicemail source. This the channel through which the source
* pulls voicemail data from a remote server.
+ *
+ * <P>Negative values are reserved to the source for source-specific states, see
+ * {@link #SOURCE_TYPE}
+ *
* <P> Possible values:
* {@link #DATA_CHANNEL_STATE_OK},
* {@link #DATA_CHANNEL_STATE_NO_CONNECTION}
@@ -431,14 +486,7 @@
* <P>Type: INTEGER</P>
*/
public static final String DATA_CHANNEL_STATE = "data_channel_state";
- /**
- * Value of {@link #DATA_CHANNEL_STATE} passed into
- * {@link #setStatus(Context, PhoneAccountHandle, int, int, int)} to indicate that the
- * {@link #DATA_CHANNEL_STATE} field is not to be changed
- *
- * @hide
- */
- public static final int DATA_CHANNEL_STATE_IGNORE = -1;
+
/**
* Value of {@link #DATA_CHANNEL_STATE} to indicate that data channel is working fine.
*/
@@ -478,6 +526,10 @@
/**
* The notification channel state of the voicemail source. This is the channel through which
* the source gets notified of new voicemails on the remote server.
+ *
+ * <P>Negative values are reserved to the source for source-specific states, see
+ * {@link #SOURCE_TYPE}
+ *
* <P> Possible values:
* {@link #NOTIFICATION_CHANNEL_STATE_OK},
* {@link #NOTIFICATION_CHANNEL_STATE_NO_CONNECTION},
@@ -486,14 +538,7 @@
* <P>Type: INTEGER</P>
*/
public static final String NOTIFICATION_CHANNEL_STATE = "notification_channel_state";
- /**
- * Value of {@link #NOTIFICATION_CHANNEL_STATE} passed into
- * {@link #setStatus(Context, PhoneAccountHandle, int, int, int)} to indicate that the
- * {@link #NOTIFICATION_CHANNEL_STATE} field is not to be changed
- *
- * @hide
- */
- public static final int NOTIFICATION_CHANNEL_STATE_IGNORE = -1;
+
/**
* Value of {@link #NOTIFICATION_CHANNEL_STATE} to indicate that the notification channel is
* working fine.
@@ -543,67 +588,5 @@
return Status.CONTENT_URI.buildUpon()
.appendQueryParameter(PARAM_KEY_SOURCE_PACKAGE, packageName).build();
}
-
- /**
- * A helper method to set the status of a voicemail source.
- *
- * @param context The context from the package calling the method. This will be the source.
- * @param accountHandle The handle for the account the source is associated with.
- * @param configurationState See {@link Status#CONFIGURATION_STATE}
- * @param dataChannelState See {@link Status#DATA_CHANNEL_STATE}
- * @param notificationChannelState See {@link Status#NOTIFICATION_CHANNEL_STATE}
- *
- * @hide
- */
- public static void setStatus(Context context, PhoneAccountHandle accountHandle,
- int configurationState, int dataChannelState, int notificationChannelState) {
- ContentValues values = new ContentValues();
- values.put(Status.PHONE_ACCOUNT_COMPONENT_NAME,
- accountHandle.getComponentName().flattenToString());
- values.put(Status.PHONE_ACCOUNT_ID, accountHandle.getId());
- if(configurationState != CONFIGURATION_STATE_IGNORE) {
- values.put(Status.CONFIGURATION_STATE, configurationState);
- }
- if(dataChannelState != DATA_CHANNEL_STATE_IGNORE) {
- values.put(Status.DATA_CHANNEL_STATE, dataChannelState);
- }
- if(notificationChannelState != NOTIFICATION_CHANNEL_STATE_IGNORE) {
- values.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
- }
- ContentResolver contentResolver = context.getContentResolver();
- Uri statusUri = buildSourceUri(context.getPackageName());
- contentResolver.insert(statusUri, values);
- }
-
- /**
- * A helper method to set the quota of a voicemail source. Unit is unspecified.
- *
- * @param context The context from the package calling the method. This will be the source.
- * @param accountHandle The handle for the account the source is associated with.
- * @param occupied See {@link Status#QUOTA_OCCUPIED}
- * @param total See {@link Status#QUOTA_TOTAL}
- *
- * @hide
- */
- public static void setQuota(Context context, PhoneAccountHandle accountHandle, int occupied,
- int total) {
- if (occupied == QUOTA_UNAVAILABLE && total == QUOTA_UNAVAILABLE) {
- return;
- }
- ContentValues values = new ContentValues();
- values.put(Status.PHONE_ACCOUNT_COMPONENT_NAME,
- accountHandle.getComponentName().flattenToString());
- values.put(Status.PHONE_ACCOUNT_ID, accountHandle.getId());
- if (occupied != QUOTA_UNAVAILABLE) {
- values.put(Status.QUOTA_OCCUPIED,occupied);
- }
- if (total != QUOTA_UNAVAILABLE) {
- values.put(Status.QUOTA_TOTAL,total);
- }
-
- ContentResolver contentResolver = context.getContentResolver();
- Uri statusUri = buildSourceUri(context.getPackageName());
- contentResolver.insert(statusUri, values);
- }
}
}
diff --git a/core/java/android/service/carrier/CarrierIdentifier.java b/core/java/android/service/carrier/CarrierIdentifier.java
index a70c24d..b47e872 100644
--- a/core/java/android/service/carrier/CarrierIdentifier.java
+++ b/core/java/android/service/carrier/CarrierIdentifier.java
@@ -126,4 +126,13 @@
mGid1 = in.readString();
mGid2 = in.readString();
}
+
+ /** @hide */
+ public interface MatchType {
+ int ALL = 0;
+ int SPN = 1;
+ int IMSI_PREFIX = 2;
+ int GID1 = 3;
+ int GID2 = 4;
+ }
}
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index de8133b..06d87f8 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -55,6 +55,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
import android.view.WindowManagerGlobal;
import java.io.FileDescriptor;
@@ -628,9 +629,9 @@
mCurWindowFlags = mWindowFlags;
mLayout.flags = mWindowFlags
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- ;
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mCurWindowPrivateFlags = mWindowPrivateFlags;
mLayout.privateFlags = mWindowPrivateFlags;
diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java
index 316c7e3..8823605 100644
--- a/core/java/android/transition/Transition.java
+++ b/core/java/android/transition/Transition.java
@@ -1941,6 +1941,26 @@
}
/**
+ * Force the transition to move to its end state, ending all the animators.
+ *
+ * @hide
+ */
+ void forceToEnd(ViewGroup sceneRoot) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ if (sceneRoot != null) {
+ WindowId windowId = sceneRoot.getWindowId();
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.view != null && windowId != null && windowId.equals(info.windowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ anim.end();
+ }
+ }
+ }
+ }
+
+ /**
* This method cancels a transition that is currently running.
*
* @hide
diff --git a/core/java/android/transition/TransitionManager.java b/core/java/android/transition/TransitionManager.java
index 71c8099..f2c871e3 100644
--- a/core/java/android/transition/TransitionManager.java
+++ b/core/java/android/transition/TransitionManager.java
@@ -440,7 +440,7 @@
ArrayList<Transition> copy = new ArrayList(runningTransitions);
for (int i = copy.size() - 1; i >= 0; i--) {
final Transition transition = copy.get(i);
- transition.end();
+ transition.forceToEnd(sceneRoot);
}
}
diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java
index 583dc0f..a41fe64 100644
--- a/core/java/android/transition/TransitionSet.java
+++ b/core/java/android/transition/TransitionSet.java
@@ -16,8 +16,6 @@
package android.transition;
-import com.android.internal.R;
-
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
@@ -26,6 +24,8 @@
import android.view.View;
import android.view.ViewGroup;
+import com.android.internal.R;
+
import java.util.ArrayList;
/**
@@ -498,6 +498,16 @@
}
}
+ /** @hide */
+ @Override
+ void forceToEnd(ViewGroup sceneRoot) {
+ super.forceToEnd(sceneRoot);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).forceToEnd(sceneRoot);
+ }
+ }
+
@Override
TransitionSet setSceneRoot(ViewGroup sceneRoot) {
super.setSceneRoot(sceneRoot);
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
index d201ade..f4db4d6 100644
--- a/core/java/android/util/DisplayMetrics.java
+++ b/core/java/android/util/DisplayMetrics.java
@@ -66,9 +66,23 @@
* {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
* instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
*/
+ public static final int DENSITY_260 = 260;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and
+ * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
+ */
public static final int DENSITY_280 = 280;
/**
+ * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and
+ * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
+ */
+ public static final int DENSITY_300 = 300;
+
+ /**
* Standard quantized DPI for extra-high-density screens.
*/
public static final int DENSITY_XHIGH = 320;
@@ -79,6 +93,14 @@
* This is not a density that applications should target, instead relying
* on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
*/
+ public static final int DENSITY_340 = 340;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
public static final int DENSITY_360 = 360;
/**
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 636384c..599c9c72 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -796,8 +796,16 @@
public static final int KEYCODE_COPY = 278;
/** Key code constant: Paste key. */
public static final int KEYCODE_PASTE = 279;
+ /** Key code constant: fingerprint navigation up */
+ public static final int KEYCODE_FP_NAV_UP = 280;
+ /** Key code constant: fingerprint navigation down */
+ public static final int KEYCODE_FP_NAV_DOWN = 281;
+ /** Key code constant: fingerprint navigation left*/
+ public static final int KEYCODE_FP_NAV_LEFT = 282;
+ /** Key code constant: fingerprint navigation right */
+ public static final int KEYCODE_FP_NAV_RIGHT = 283;
- private static final int LAST_KEYCODE = KEYCODE_PASTE;
+ private static final int LAST_KEYCODE = KEYCODE_FP_NAV_RIGHT;
// NOTE: If you add a new keycode here you must also add it to:
// isSystem()
@@ -1844,6 +1852,10 @@
case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
case KeyEvent.KEYCODE_BRIGHTNESS_UP:
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ case KeyEvent.KEYCODE_FP_NAV_UP:
+ case KeyEvent.KEYCODE_FP_NAV_DOWN:
+ case KeyEvent.KEYCODE_FP_NAV_LEFT:
+ case KeyEvent.KEYCODE_FP_NAV_RIGHT:
return true;
}
diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java
index ab4cbcf..0164fcd 100644
--- a/core/java/android/view/RenderNode.java
+++ b/core/java/android/view/RenderNode.java
@@ -793,12 +793,12 @@
return mOwningView != null && mOwningView.mAttachInfo != null;
}
- public void addAnimator(AnimatedVectorDrawable.VectorDrawableAnimatorRT animatorSet) {
+ public void registerVectorDrawableAnimator(
+ AnimatedVectorDrawable.VectorDrawableAnimatorRT animatorSet) {
if (mOwningView == null || mOwningView.mAttachInfo == null) {
throw new IllegalStateException("Cannot start this animator on a detached view!");
}
- nAddAnimator(mNativeRenderNode, animatorSet.getAnimatorNativePtr());
- mOwningView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(this);
+ mOwningView.mAttachInfo.mViewRootImpl.registerVectorDrawableAnimator(animatorSet);
}
public void endAllAnimators() {
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index e650d95..fcca739 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -23,6 +23,7 @@
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -881,6 +882,12 @@
nRegisterAnimatingRenderNode(mRootNode.mNativeRenderNode, animator.mNativeRenderNode);
}
+ void registerVectorDrawableAnimator(
+ AnimatedVectorDrawable.VectorDrawableAnimatorRT animator) {
+ nRegisterVectorDrawableAnimator(mRootNode.mNativeRenderNode,
+ animator.getAnimatorNativePtr());
+ }
+
public void serializeDisplayListTree() {
nSerializeDisplayListTree(mNativeProxy);
}
@@ -992,6 +999,7 @@
private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size);
private static native void nDestroy(long nativeProxy, long rootRenderNode);
private static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode);
+ private static native void nRegisterVectorDrawableAnimator(long rootRenderNode, long animator);
private static native void nInvokeFunctor(long functor, boolean waitForCompletion);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 48bdcb2..1186569 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -42,6 +42,7 @@
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
@@ -824,6 +825,13 @@
}
}
+ public void registerVectorDrawableAnimator(
+ AnimatedVectorDrawable.VectorDrawableAnimatorRT animator) {
+ if (mAttachInfo.mHardwareRenderer != null) {
+ mAttachInfo.mHardwareRenderer.registerVectorDrawableAnimator(animator);
+ }
+ }
+
private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
mAttachInfo.mHardwareAccelerated = false;
mAttachInfo.mHardwareAccelerationRequested = false;
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index 89dec2d..0ebbf7f 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -851,4 +851,9 @@
endBatchEdit();
}
+
+ /**
+ * The default implementation does nothing.
+ */
+ public boolean commitContent(InputContentInfo inputContentInfo, Bundle opts) { return false; }
}
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 7b7ccae..2dca892 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -25,6 +25,8 @@
import android.text.TextUtils;
import android.util.Printer;
+import java.util.Arrays;
+
/**
* An EditorInfo describes several attributes of a text editing object
* that an input method is communicating with (typically an EditText), most
@@ -363,6 +365,18 @@
@Nullable
public LocaleList hintLocales = null;
+
+ /**
+ * List of acceptable MIME types for
+ * {@link InputConnection#commitContent(InputContentInfo, Bundle)}.
+ *
+ * <p>{@code null} or an empty array means that
+ * {@link InputConnection#commitContent(InputContentInfo, Bundle)} is not supported in this
+ * editor.</p>
+ */
+ @Nullable
+ public String[] contentMimeTypes = null;
+
/**
* Ensure that the data in this EditorInfo is compatible with an application
* that was developed against the given target API version. This can
@@ -418,6 +432,7 @@
+ " fieldName=" + fieldName);
pw.println(prefix + "extras=" + extras);
pw.println(prefix + "hintLocales=" + hintLocales);
+ pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes));
}
/**
@@ -446,6 +461,7 @@
} else {
LocaleList.getEmptyLocaleList().writeToParcel(dest, flags);
}
+ dest.writeStringArray(contentMimeTypes);
}
/**
@@ -471,6 +487,7 @@
res.extras = source.readBundle();
LocaleList hintLocales = LocaleList.CREATOR.createFromParcel(source);
res.hintLocales = hintLocales.isEmpty() ? null : hintLocales;
+ res.contentMimeTypes = source.readStringArray();
return res;
}
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index 9f66429..f7f3694 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -16,6 +16,8 @@
package android.view.inputmethod;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyCharacterMap;
@@ -836,4 +838,33 @@
* <p>Note: This does nothing when called from input methods.</p>
*/
public void closeConnection();
+
+ /**
+ * Called by the input method to commit a content such as PNG image to the editor.
+ *
+ * <p>In order to avoid variety of compatibility issues, this focuses on a simple use case,
+ * where we expect editors and IMEs work cooperatively as follows:</p>
+ * <ul>
+ * <li>Editor must keep {@link EditorInfo#contentMimeTypes} to be {@code null} if it does
+ * not support this method at all.</li>
+ * <li>Editor can ignore this request when the MIME type specified in
+ * {@code inputContentInfo} does not match to any of {@link EditorInfo#contentMimeTypes}.
+ * </li>
+ * <li>Editor can ignore the cursor position when inserting the provided context.</li>
+ * <li>Editor can return {@code true} asynchronously, even before it starts loading the
+ * content.</li>
+ * <li>Editor should provide a way to delete the content inserted by this method, or revert
+ * the effect caused by this method.</li>
+ * <li>IME should not call this method when there is any composing text, in case calling
+ * this method causes focus change.</li>
+ * <li>IME should grant a permission for the editor to read the content. See
+ * {@link EditorInfo#packageName} about how to obtain the package name of the editor.</li>
+ * </ul>
+ *
+ * @param inputContentInfo Content to be inserted.
+ * @param opts optional bundle data. This can be {@code null}.
+ * @return {@code true} if this request is accepted by the application, no matter if the request
+ * is already handled or still being handled in background.
+ */
+ public boolean commitContent(@NonNull InputContentInfo inputContentInfo, @Nullable Bundle opts);
}
diff --git a/core/java/android/view/inputmethod/InputConnectionInspector.java b/core/java/android/view/inputmethod/InputConnectionInspector.java
index 118a61f..727e9ca 100644
--- a/core/java/android/view/inputmethod/InputConnectionInspector.java
+++ b/core/java/android/view/inputmethod/InputConnectionInspector.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Bundle;
import java.lang.annotation.Retention;
import java.lang.reflect.Method;
@@ -41,6 +42,8 @@
MissingMethodFlags.REQUEST_CURSOR_UPDATES,
MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS,
MissingMethodFlags.GET_HANDLER,
+ MissingMethodFlags.CLOSE_CONNECTION,
+ MissingMethodFlags.COMMIT_CONTENT,
})
public @interface MissingMethodFlags {
/**
@@ -78,6 +81,11 @@
* {@link android.os.Build.VERSION_CODES#N} and later.
*/
int CLOSE_CONNECTION = 1 << 6;
+ /**
+ * {@link InputConnection#commitContent(InputContentInfo, Bundle)} is available in
+ * {@link android.os.Build.VERSION_CODES#N} MR-1 and later.
+ */
+ int COMMIT_CONTENT = 1 << 7;
}
private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
@@ -127,6 +135,9 @@
if (!hasCloseConnection(clazz)) {
flags |= MissingMethodFlags.CLOSE_CONNECTION;
}
+ if (!hasCommitContent(clazz)) {
+ flags |= MissingMethodFlags.COMMIT_CONTENT;
+ }
sMissingMethodsMap.put(clazz, flags);
return flags;
}
@@ -195,6 +206,16 @@
}
}
+ private static boolean hasCommitContent(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("commitContent", InputContentInfo.class,
+ Bundle.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) {
final StringBuilder sb = new StringBuilder();
boolean isEmpty = true;
@@ -242,6 +263,12 @@
}
sb.append("closeConnection()");
}
+ if ((flags & MissingMethodFlags.COMMIT_CONTENT) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("commitContent(InputContentInfo, Bundle)");
+ }
return sb.toString();
}
}
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index e743f62..af9bcae 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -269,4 +269,12 @@
public void closeConnection() {
mTarget.closeConnection();
}
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ public boolean commitContent(InputContentInfo inputContentInfo, Bundle opts) {
+ return mTarget.commitContent(inputContentInfo, opts);
+ }
}
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl b/core/java/android/view/inputmethod/InputContentInfo.aidl
similarity index 73%
copy from telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
copy to core/java/android/view/inputmethod/InputContentInfo.aidl
index b7e78d1..1afeee3b 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
+++ b/core/java/android/view/inputmethod/InputContentInfo.aidl
@@ -1,11 +1,11 @@
/*
- * Copyright 2016, The Android Open Source Project
+ * 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
+ * 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,
@@ -14,9 +14,6 @@
* limitations under the License.
*/
-package android.telecom;
+package android.view.inputmethod;
-/**
- * {@hide}
- */
-parcelable ParcelableCallAnalytics;
+parcelable InputContentInfo;
diff --git a/core/java/android/view/inputmethod/InputContentInfo.java b/core/java/android/view/inputmethod/InputContentInfo.java
new file mode 100644
index 0000000..e2ecfae
--- /dev/null
+++ b/core/java/android/view/inputmethod/InputContentInfo.java
@@ -0,0 +1,194 @@
+/*
+ * 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.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.security.InvalidParameterException;
+
+/**
+ * A container object with which input methods can send content files to the target application.
+ */
+public class InputContentInfo implements Parcelable {
+
+ @NonNull
+ private final Uri mContentUri;
+ @NonNull
+ private final ClipDescription mDescription;
+ @Nullable
+ private final Uri mLinkUri;
+
+ /**
+ * Constructs {@link InputContentInfo} object only with mandatory data.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ */
+ public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description) {
+ this(contentUri, description, null /* link Uri */);
+ }
+
+ /**
+ * Constructs {@link InputContentInfo} object with additional link URI.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
+ * a way to navigate the user to the specified web page if this is not {@code null}.
+ * @throws InvalidParameterException if any invalid parameter is specified.
+ */
+ public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description,
+ @Nullable Uri linkUri) {
+ validateInternal(contentUri, description, linkUri, true /* throwException */);
+ mContentUri = contentUri;
+ mDescription = description;
+ mLinkUri = linkUri;
+ }
+
+ /**
+ * @return {@code true} if all the fields are valid.
+ * @hide
+ */
+ public boolean validate() {
+ return validateInternal(mContentUri, mDescription, mLinkUri, false /* throwException */);
+ }
+
+ /**
+ * Constructs {@link InputContentInfo} object with additional link URI.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
+ * a way to navigate the user to the specified web page if this is not {@code null}.
+ * @param throwException {@code true} if this method should throw an
+ * {@link InvalidParameterException}.
+ * @throws InvalidParameterException if any invalid parameter is specified.
+ */
+ private static boolean validateInternal(@NonNull Uri contentUri,
+ @NonNull ClipDescription description, @Nullable Uri linkUri, boolean throwException) {
+ if (contentUri == null) {
+ if (throwException) {
+ throw new NullPointerException("contentUri");
+ }
+ return false;
+ }
+ if (description == null) {
+ if (throwException) {
+ throw new NullPointerException("description");
+ }
+ return false;
+ }
+ final String contentUriScheme = contentUri.getScheme();
+ if (contentUriScheme == null || !contentUriScheme.equalsIgnoreCase("content")) {
+ if (throwException) {
+ throw new InvalidParameterException("contentUri must have content scheme");
+ }
+ return false;
+ }
+ if (linkUri != null) {
+ final String scheme = linkUri.getScheme();
+ if (scheme == null ||
+ (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
+ if (throwException) {
+ throw new InvalidParameterException(
+ "linkUri must have either http or https scheme");
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return Content URI with which the content can be obtained.
+ */
+ @NonNull
+ public Uri getContentUri() { return mContentUri; }
+
+ /**
+ * @return {@link ClipDescription} object that contains the metadata of {@code contentUri} such
+ * as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility purpose.
+ */
+ @NonNull
+ public ClipDescription getDescription() { return mDescription; }
+
+ /**
+ * @return An optional {@code http} or {@code https} URI that is related to this content.
+ */
+ @Nullable
+ public Uri getLinkUri() { return mLinkUri; }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Uri.writeToParcel(dest, mContentUri);
+ mDescription.writeToParcel(dest, flags);
+ Uri.writeToParcel(dest, mLinkUri);
+ }
+
+ private InputContentInfo(@NonNull Parcel source) {
+ mContentUri = Uri.CREATOR.createFromParcel(source);
+ mDescription = ClipDescription.CREATOR.createFromParcel(source);
+ mLinkUri = Uri.CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<InputContentInfo> CREATOR
+ = new Parcelable.Creator<InputContentInfo>() {
+ @Override
+ public InputContentInfo createFromParcel(Parcel source) {
+ return new InputContentInfo(source);
+ }
+
+ @Override
+ public InputContentInfo[] newArray(int size) {
+ return new InputContentInfo[size];
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index b331be7..f296421 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -75,6 +75,7 @@
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.RemoteViews.OnClickHandler;
@@ -5982,6 +5983,11 @@
public void closeConnection() {
getTarget().closeConnection();
}
+
+ @Override
+ public boolean commitContent(InputContentInfo inputContentInfo, Bundle opts) {
+ return getTarget().commitContent(inputContentInfo, opts);
+ }
}
/**
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 1f745185..c33288b 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -3195,7 +3195,9 @@
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
- return inflater.inflate(rv.getLayoutId(), parent, false);
+ View v = inflater.inflate(rv.getLayoutId(), parent, false);
+ v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
+ return v;
}
private static void loadTransitionOverride(Context context,
@@ -3373,7 +3375,7 @@
// across orientation change, and has the RemoteViews re-applied in the new orientation,
// we throw an exception, since the layouts may be completely unrelated.
if (hasLandscapeAndPortraitLayouts()) {
- if (v.getId() != rvToApply.getLayoutId()) {
+ if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
" that does not share the same root layout id.");
}
@@ -3409,7 +3411,7 @@
// across orientation change, and has the RemoteViews re-applied in the new orientation,
// we throw an exception, since the layouts may be completely unrelated.
if (hasLandscapeAndPortraitLayouts()) {
- if (v.getId() != rvToApply.getLayoutId()) {
+ if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
" that does not share the same root layout id.");
}
diff --git a/core/java/com/android/internal/app/AlertActivity.java b/core/java/com/android/internal/app/AlertActivity.java
index ed48b0d..35ffa71 100644
--- a/core/java/com/android/internal/app/AlertActivity.java
+++ b/core/java/com/android/internal/app/AlertActivity.java
@@ -49,7 +49,7 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mAlert = new AlertController(this, this, getWindow());
+ mAlert = AlertController.create(this, this, getWindow());
mAlertParams = new AlertController.AlertParams(this);
}
diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java
index b7ac600..2a40aeb 100644
--- a/core/java/com/android/internal/app/AlertController.java
+++ b/core/java/com/android/internal/app/AlertController.java
@@ -24,6 +24,7 @@
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
@@ -61,14 +62,15 @@
import java.lang.ref.WeakReference;
public class AlertController {
+ public static final int MICRO = 1;
private final Context mContext;
private final DialogInterface mDialogInterface;
- private final Window mWindow;
+ protected final Window mWindow;
private CharSequence mTitle;
- private CharSequence mMessage;
- private ListView mListView;
+ protected CharSequence mMessage;
+ protected ListView mListView;
private View mView;
private int mViewLayoutResId;
@@ -91,14 +93,14 @@
private CharSequence mButtonNeutralText;
private Message mButtonNeutralMessage;
- private ScrollView mScrollView;
+ protected ScrollView mScrollView;
private int mIconId = 0;
private Drawable mIcon;
private ImageView mIconView;
private TextView mTitleView;
- private TextView mMessageView;
+ protected TextView mMessageView;
private View mCustomTitleView;
private boolean mForceInverseBackground;
@@ -176,7 +178,26 @@
return outValue.data != 0;
}
- public AlertController(Context context, DialogInterface di, Window window) {
+ private static boolean isWatch(Context context) {
+ return (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_TYPE_WATCH)
+ == Configuration.UI_MODE_TYPE_WATCH;
+ }
+
+ public static final AlertController create(Context context, DialogInterface di, Window window) {
+ final TypedArray a = context.obtainStyledAttributes(
+ null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+ int controllerType = a.getInt(R.styleable.AlertDialog_controllerType, 0);
+ a.recycle();
+
+ switch (controllerType) {
+ case MICRO:
+ return new MicroAlertController(context, di, window);
+ default:
+ return new AlertController(context, di, window);
+ }
+ }
+
+ protected AlertController(Context context, DialogInterface di, Window window) {
mContext = context;
mDialogInterface = di;
mWindow = window;
@@ -643,7 +664,7 @@
}
}
- private void setupContent(ViewGroup contentPanel) {
+ protected void setupContent(ViewGroup contentPanel) {
mScrollView = (ScrollView) contentPanel.findViewById(R.id.scrollView);
mScrollView.setFocusable(false);
@@ -871,8 +892,14 @@
listView.setAdapter(mAdapter);
final int checkedItem = mCheckedItem;
if (checkedItem > -1) {
- listView.setItemChecked(checkedItem, true);
- listView.setSelection(checkedItem);
+ // TODO: Remove temp watch specific code
+ if (isWatch(mContext)) {
+ listView.setItemChecked(checkedItem + listView.getHeaderViewsCount(), true);
+ listView.setSelection(checkedItem + listView.getHeaderViewsCount());
+ } else {
+ listView.setItemChecked(checkedItem, true);
+ listView.setSelection(checkedItem);
+ }
}
}
}
@@ -1051,7 +1078,13 @@
if (mCheckedItems != null) {
boolean isItemChecked = mCheckedItems[position];
if (isItemChecked) {
- listView.setItemChecked(position, true);
+ // TODO: Remove temp watch specific code
+ if (isWatch(mContext)) {
+ listView.setItemChecked(
+ position + listView.getHeaderViewsCount(), true);
+ } else {
+ listView.setItemChecked(position, true);
+ }
}
}
return view;
@@ -1072,8 +1105,16 @@
public void bindView(View view, Context context, Cursor cursor) {
CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1);
text.setText(cursor.getString(mLabelIndex));
- listView.setItemChecked(cursor.getPosition(),
- cursor.getInt(mIsCheckedIndex) == 1);
+ // TODO: Remove temp watch specific code
+ if (isWatch(mContext)) {
+ listView.setItemChecked(
+ cursor.getPosition() + listView.getHeaderViewsCount(),
+ cursor.getInt(mIsCheckedIndex) == 1);
+ } else {
+ listView.setItemChecked(
+ cursor.getPosition(),
+ cursor.getInt(mIsCheckedIndex) == 1);
+ }
}
@Override
@@ -1116,6 +1157,10 @@
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ // TODO: Remove temp watch specific code
+ if (isWatch(mContext)) {
+ position -= listView.getHeaderViewsCount();
+ }
mOnClickListener.onClick(dialog.mDialogInterface, position);
if (!mIsSingleChoice) {
dialog.mDialogInterface.dismiss();
@@ -1126,6 +1171,10 @@
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ // TODO: Remove temp watch specific code
+ if (isWatch(mContext)) {
+ position -= listView.getHeaderViewsCount();
+ }
if (mCheckedItems != null) {
mCheckedItems[position] = listView.isItemChecked(position);
}
diff --git a/core/java/com/android/internal/app/EphemeralResolverService.java b/core/java/com/android/internal/app/EphemeralResolverService.java
index 6ba04a9..68724a7 100644
--- a/core/java/com/android/internal/app/EphemeralResolverService.java
+++ b/core/java/com/android/internal/app/EphemeralResolverService.java
@@ -39,14 +39,19 @@
public abstract class EphemeralResolverService extends Service {
public static final String EXTRA_RESOLVE_INFO = "com.android.internal.app.RESOLVE_INFO";
public static final String EXTRA_SEQUENCE = "com.android.internal.app.SEQUENCE";
+ private static final String EXTRA_PREFIX = "com.android.internal.app.PREFIX";
private Handler mHandler;
/**
* Called to retrieve resolve info for ephemeral applications.
*
* @param digestPrefix The hash prefix of the ephemeral's domain.
+ * @param prefixMask A mask that was applied to each digest prefix. This should
+ * be used when comparing against the digest prefixes as all bits might
+ * not be set.
*/
- protected abstract List<EphemeralResolveInfo> getEphemeralResolveInfoList(int digestPrefix);
+ protected abstract List<EphemeralResolveInfo> getEphemeralResolveInfoList(
+ int digestPrefix[], int prefixMask);
@Override
protected final void attachBaseContext(Context base) {
@@ -59,10 +64,13 @@
return new IEphemeralResolver.Stub() {
@Override
public void getEphemeralResolveInfoList(
- IRemoteCallback callback, int digestPrefix, int sequence) {
- mHandler.obtainMessage(ServiceHandler.MSG_GET_EPHEMERAL_RESOLVE_INFO,
- digestPrefix, sequence, callback)
- .sendToTarget();
+ IRemoteCallback callback, int digestPrefix[], int prefixMask, int sequence) {
+ final Message msg = mHandler.obtainMessage(
+ ServiceHandler.MSG_GET_EPHEMERAL_RESOLVE_INFO, prefixMask, sequence, callback);
+ final Bundle data = new Bundle();
+ data.putIntArray(EXTRA_PREFIX, digestPrefix);
+ msg.setData(data);
+ msg.sendToTarget();
}
};
}
@@ -81,8 +89,9 @@
switch (action) {
case MSG_GET_EPHEMERAL_RESOLVE_INFO: {
final IRemoteCallback callback = (IRemoteCallback) message.obj;
+ final int[] digestPrefix = message.getData().getIntArray(EXTRA_PREFIX);
final List<EphemeralResolveInfo> resolveInfo =
- getEphemeralResolveInfoList(message.arg1);
+ getEphemeralResolveInfoList(digestPrefix, message.arg1);
final Bundle data = new Bundle();
data.putInt(EXTRA_SEQUENCE, message.arg2);
data.putParcelableList(EXTRA_RESOLVE_INFO, resolveInfo);
diff --git a/core/java/com/android/internal/app/IEphemeralResolver.aidl b/core/java/com/android/internal/app/IEphemeralResolver.aidl
index 40429ee..9ff1322 100644
--- a/core/java/com/android/internal/app/IEphemeralResolver.aidl
+++ b/core/java/com/android/internal/app/IEphemeralResolver.aidl
@@ -20,5 +20,6 @@
import android.os.IRemoteCallback;
oneway interface IEphemeralResolver {
- void getEphemeralResolveInfoList(IRemoteCallback callback, int digestPrefix, int sequence);
+ void getEphemeralResolveInfoList(IRemoteCallback callback, in int[] digestPrefix,
+ int prefixMask, int sequence);
}
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index e782c3c..e3fce51 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -307,9 +307,6 @@
localizedLocales.add(li.getLangScriptKey());
}
- // Serbian in Latin script is only partially localized in N.
- localizedLocales.remove("sr-Latn");
-
for (LocaleInfo li : sLocaleCache.values()) {
li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
}
diff --git a/core/java/com/android/internal/app/MicroAlertController.java b/core/java/com/android/internal/app/MicroAlertController.java
new file mode 100644
index 0000000..085b226
--- /dev/null
+++ b/core/java/com/android/internal/app/MicroAlertController.java
@@ -0,0 +1,85 @@
+/*
+ * 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.internal.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.AbsListView;
+
+import com.android.internal.app.AlertController;
+import com.android.internal.R;
+
+public class MicroAlertController extends AlertController {
+ public MicroAlertController(Context context, DialogInterface di, Window window) {
+ super(context, di, window);
+ }
+
+ @Override
+ protected void setupContent(ViewGroup contentPanel) {
+ // Special case for small screen - the scroll view is higher in hierarchy
+ mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView);
+
+ // Special case for users that only want to display a String
+ mMessageView = (TextView) contentPanel.findViewById(R.id.message);
+ if (mMessageView == null) {
+ return;
+ }
+
+ if (mMessage != null) {
+ mMessageView.setText(mMessage);
+ } else {
+ // no message, remove associated views
+ mMessageView.setVisibility(View.GONE);
+ contentPanel.removeView(mMessageView);
+
+ if (mListView != null) {
+ // has ListView, swap ScrollView with ListView
+
+ // move topPanel into header of ListView
+ View topPanel = mScrollView.findViewById(R.id.topPanel);
+ ((ViewGroup) topPanel.getParent()).removeView(topPanel);
+ topPanel.setLayoutParams(
+ new AbsListView.LayoutParams(topPanel.getLayoutParams()));
+ mListView.addHeaderView(topPanel, null, false);
+
+ // move buttonPanel into footer of ListView
+ View buttonPanel = mScrollView.findViewById(R.id.buttonPanel);
+ ((ViewGroup) buttonPanel.getParent()).removeView(buttonPanel);
+ buttonPanel.setLayoutParams(
+ new AbsListView.LayoutParams(buttonPanel.getLayoutParams()));
+ mListView.addFooterView(buttonPanel, null, false);
+
+ // swap ScrollView w/ ListView
+ final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent();
+ final int childIndex = scrollParent.indexOfChild(mScrollView);
+ scrollParent.removeViewAt(childIndex);
+ scrollParent.addView(mListView, childIndex,
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ } else {
+ // no content, just hide everything
+ contentPanel.setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 085e159..0a4ac0d 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -69,9 +69,13 @@
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.MetricsProto;
import com.android.internal.widget.ResolverDrawerLayout;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
@@ -359,6 +363,12 @@
if (isVoiceInteraction()) {
onSetupVoiceInteraction();
}
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this, mAdapter.hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray()) : ""));
}
public final void setFilteredComponents(ComponentName[] components) {
@@ -649,6 +659,19 @@
TargetInfo target = mAdapter.targetInfoForPosition(which, filtered);
if (onTargetSelected(target, always)) {
+ if (always && filtered) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+ } else if (filtered) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+ } else {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ }
+ MetricsLogger.action(this, mAdapter.hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
finish();
}
}
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index 4260e50..951a45a 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -34,7 +34,7 @@
// for AppWidgetHost
//
ParceledListSlice startListening(IAppWidgetHost host, String callingPackage, int hostId,
- in int[] appWidgetIds, out int[] updatedIds);
+ in int[] appWidgetIds);
void stopListening(String callingPackage, int hostId);
int allocateAppWidgetId(String callingPackage, int hostId);
void deleteAppWidgetId(String callingPackage, int appWidgetId);
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 9c960c0..73a3a0b 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -753,7 +753,7 @@
closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
- } catch (RuntimeException ex) {
+ } catch (Throwable ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
diff --git a/core/java/com/android/internal/policy/BackdropFrameRenderer.java b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
index 28281b3c..8addffb0 100644
--- a/core/java/com/android/internal/policy/BackdropFrameRenderer.java
+++ b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
@@ -370,6 +370,8 @@
systemInsets.bottom);
final int rightInset = DecorView.getColorViewRightInset(stableInsets.right,
systemInsets.right);
+ final int leftInset = DecorView.getColorViewLeftInset(stableInsets.left,
+ systemInsets.left);
if (mStatusBarColor != null) {
mStatusBarColor.setBounds(0, 0, left + width, topInset);
mStatusBarColor.draw(canvas);
@@ -379,9 +381,11 @@
// don't want the navigation bar background be moving around when resizing in docked mode.
// However, we need it for the transitions into/out of docked mode.
if (mNavigationBarColor != null && fullscreen) {
- final int size = DecorView.getNavBarSize(bottomInset, rightInset);
+ final int size = DecorView.getNavBarSize(bottomInset, rightInset, leftInset);
if (DecorView.isNavBarToRightEdge(bottomInset, rightInset)) {
mNavigationBarColor.setBounds(width - size, 0, width, height);
+ } else if (DecorView.isNavBarToLeftEdge(bottomInset, rightInset)) {
+ mNavigationBarColor.setBounds(0, 0, size, height);
} else {
mNavigationBarColor.setBounds(0, height - size, width, height);
}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 3cf7a4e..366fc1a 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -97,7 +97,6 @@
import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -162,13 +161,13 @@
private final ColorViewState mStatusColorViewState = new ColorViewState(
SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
- Gravity.TOP, Gravity.LEFT,
+ Gravity.TOP, Gravity.LEFT, Gravity.RIGHT,
Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME,
com.android.internal.R.id.statusBarBackground,
FLAG_FULLSCREEN);
private final ColorViewState mNavigationColorViewState = new ColorViewState(
SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION,
- Gravity.BOTTOM, Gravity.RIGHT,
+ Gravity.BOTTOM, Gravity.RIGHT, Gravity.LEFT,
Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME,
com.android.internal.R.id.navigationBarBackground,
0 /* hideWindowFlag */);
@@ -184,9 +183,11 @@
private int mLastTopInset = 0;
private int mLastBottomInset = 0;
private int mLastRightInset = 0;
+ private int mLastLeftInset = 0;
private boolean mLastHasTopStableInset = false;
private boolean mLastHasBottomStableInset = false;
private boolean mLastHasRightStableInset = false;
+ private boolean mLastHasLeftStableInset = false;
private int mLastWindowFlags = 0;
private boolean mLastShouldAlwaysConsumeNavBar = false;
@@ -991,12 +992,21 @@
return Math.min(stableRight, systemRight);
}
+ static int getColorViewLeftInset(int stableLeft, int systemLeft) {
+ return Math.min(stableLeft, systemLeft);
+ }
+
static boolean isNavBarToRightEdge(int bottomInset, int rightInset) {
return bottomInset == 0 && rightInset > 0;
}
- static int getNavBarSize(int bottomInset, int rightInset) {
- return isNavBarToRightEdge(bottomInset, rightInset) ? rightInset : bottomInset;
+ static boolean isNavBarToLeftEdge(int bottomInset, int leftInset) {
+ return bottomInset == 0 && leftInset > 0;
+ }
+
+ static int getNavBarSize(int bottomInset, int rightInset, int leftInset) {
+ return isNavBarToRightEdge(bottomInset, rightInset) ? rightInset
+ : isNavBarToLeftEdge(bottomInset, leftInset) ? leftInset : bottomInset;
}
WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
@@ -1016,6 +1026,8 @@
insets.getSystemWindowInsetBottom());
mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
insets.getSystemWindowInsetRight());
+ mLastLeftInset = getColorViewRightInset(insets.getStableInsetLeft(),
+ insets.getSystemWindowInsetLeft());
// Don't animate if the presence of stable insets has changed, because that
// indicates that the window was either just added and received them for the
@@ -1031,21 +1043,32 @@
boolean hasRightStableInset = insets.getStableInsetRight() != 0;
disallowAnimate |= (hasRightStableInset != mLastHasRightStableInset);
mLastHasRightStableInset = hasRightStableInset;
+
+ boolean hasLeftStableInset = insets.getStableInsetLeft() != 0;
+ disallowAnimate |= (hasLeftStableInset != mLastHasLeftStableInset);
+ mLastHasLeftStableInset = hasLeftStableInset;
+
mLastShouldAlwaysConsumeNavBar = insets.shouldAlwaysConsumeNavBar();
}
boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset);
- int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset);
+ boolean navBarToLeftEdge = isNavBarToLeftEdge(mLastBottomInset, mLastLeftInset);
+ int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
- mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge,
- 0 /* rightInset */, animate && !disallowAnimate, false /* force */);
+ mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge || navBarToLeftEdge,
+ navBarToLeftEdge,
+ 0 /* sideInset */, animate && !disallowAnimate, false /* force */);
boolean statusBarNeedsRightInset = navBarToRightEdge
&& mNavigationColorViewState.present;
- int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0;
+ boolean statusBarNeedsLeftInset = navBarToLeftEdge
+ && mNavigationColorViewState.present;
+ int statusBarSideInset = statusBarNeedsRightInset ? mLastRightInset
+ : statusBarNeedsLeftInset ? mLastLeftInset : 0;
updateColorViewInt(mStatusColorViewState, sysUiVisibility,
calculateStatusBarColor(), mLastTopInset,
- false /* matchVertical */, statusBarRightInset, animate && !disallowAnimate,
+ false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
+ animate && !disallowAnimate,
mForceWindowDrawsStatusBarBackground);
}
@@ -1070,15 +1093,17 @@
int consumedTop = consumingStatusBar ? mLastTopInset : 0;
int consumedRight = consumingNavBar ? mLastRightInset : 0;
int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
+ int consumedLeft = consumingNavBar ? mLastLeftInset : 0;
if (mContentRoot != null
&& mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
- || lp.bottomMargin != consumedBottom) {
+ || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
lp.topMargin = consumedTop;
lp.rightMargin = consumedRight;
lp.bottomMargin = consumedBottom;
+ lp.leftMargin = consumedLeft;
mContentRoot.setLayoutParams(lp);
if (insets == null) {
@@ -1089,7 +1114,7 @@
}
if (insets != null) {
insets = insets.replaceSystemWindowInsets(
- insets.getSystemWindowInsetLeft(),
+ insets.getSystemWindowInsetLeft() - consumedLeft,
insets.getSystemWindowInsetTop() - consumedTop,
insets.getSystemWindowInsetRight() - consumedRight,
insets.getSystemWindowInsetBottom() - consumedBottom);
@@ -1126,11 +1151,12 @@
* @param size the current size in the non-parent-matching dimension.
* @param verticalBar if true the view is attached to a vertical edge, otherwise to a
* horizontal edge,
- * @param rightMargin rightMargin for the color view.
+ * @param sideMargin sideMargin for the color view.
* @param animate if true, the change will be animated.
*/
private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
- int size, boolean verticalBar, int rightMargin, boolean animate, boolean force) {
+ int size, boolean verticalBar, boolean seascape, int sideMargin,
+ boolean animate, boolean force) {
state.present = (sysUiVis & state.systemUiHideFlag) == 0
&& (mWindow.getAttributes().flags & state.hideWindowFlag) == 0
&& ((mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
@@ -1145,7 +1171,9 @@
int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
- int resolvedGravity = verticalBar ? state.horizontalGravity : state.verticalGravity;
+ int resolvedGravity = verticalBar
+ ? (seascape ? state.seascapeGravity : state.horizontalGravity)
+ : state.verticalGravity;
if (view == null) {
if (showView) {
@@ -1159,7 +1187,11 @@
LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
resolvedGravity);
- lp.rightMargin = rightMargin;
+ if (seascape) {
+ lp.leftMargin = sideMargin;
+ } else {
+ lp.rightMargin = sideMargin;
+ }
addView(view, lp);
updateColorViewTranslations();
}
@@ -1168,12 +1200,16 @@
visibilityChanged = state.targetVisibility != vis;
state.targetVisibility = vis;
LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ int rightMargin = seascape ? 0 : sideMargin;
+ int leftMargin = seascape ? sideMargin : 0;
if (lp.height != resolvedHeight || lp.width != resolvedWidth
- || lp.gravity != resolvedGravity || lp.rightMargin != rightMargin) {
+ || lp.gravity != resolvedGravity || lp.rightMargin != rightMargin
+ || lp.leftMargin != leftMargin) {
lp.height = resolvedHeight;
lp.width = resolvedWidth;
lp.gravity = resolvedGravity;
lp.rightMargin = rightMargin;
+ lp.leftMargin = leftMargin;
view.setLayoutParams(lp);
}
if (showView) {
@@ -2217,17 +2253,19 @@
final int translucentFlag;
final int verticalGravity;
final int horizontalGravity;
+ final int seascapeGravity;
final String transitionName;
final int hideWindowFlag;
ColorViewState(int systemUiHideFlag,
int translucentFlag, int verticalGravity, int horizontalGravity,
- String transitionName, int id, int hideWindowFlag) {
+ int seascapeGravity, String transitionName, int id, int hideWindowFlag) {
this.id = id;
this.systemUiHideFlag = systemUiHideFlag;
this.translucentFlag = translucentFlag;
this.verticalGravity = verticalGravity;
this.horizontalGravity = horizontalGravity;
+ this.seascapeGravity = seascapeGravity;
this.transitionName = transitionName;
this.hideWindowFlag = hideWindowFlag;
}
diff --git a/core/java/com/android/internal/util/WakeupMessage.java b/core/java/com/android/internal/util/WakeupMessage.java
index 2653745..7d222c7 100644
--- a/core/java/com/android/internal/util/WakeupMessage.java
+++ b/core/java/com/android/internal/util/WakeupMessage.java
@@ -45,24 +45,32 @@
protected final String mCmdName;
@VisibleForTesting
protected final int mCmd, mArg1, mArg2;
+ @VisibleForTesting
+ protected final Object mObj;
private boolean mScheduled;
public WakeupMessage(Context context, Handler handler,
- String cmdName, int cmd, int arg1, int arg2) {
+ String cmdName, int cmd, int arg1, int arg2, Object obj) {
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mHandler = handler;
mCmdName = cmdName;
mCmd = cmd;
mArg1 = arg1;
mArg2 = arg2;
+ mObj = obj;
}
public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) {
- this(context, handler, cmdName, cmd, arg1, 0);
+ this(context, handler, cmdName, cmd, arg1, 0, null);
+ }
+
+ public WakeupMessage(Context context, Handler handler,
+ String cmdName, int cmd, int arg1, int arg2) {
+ this(context, handler, cmdName, cmd, arg1, arg2, null);
}
public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
- this(context, handler, cmdName, cmd, 0, 0);
+ this(context, handler, cmdName, cmd, 0, 0, null);
}
/**
@@ -99,7 +107,7 @@
mScheduled = false;
}
if (stillScheduled) {
- Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2);
+ Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
mHandler.handleMessage(msg);
msg.recycle();
}
diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java
index 59b4b35..dce9d2c 100644
--- a/core/java/com/android/internal/view/IInputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java
@@ -33,6 +33,7 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionInspector;
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
+import android.view.inputmethod.InputContentInfo;
public abstract class IInputConnectionWrapper extends IInputContext.Stub {
static final String TAG = "IInputConnectionWrapper";
@@ -61,6 +62,7 @@
private static final int DO_CLEAR_META_KEY_STATES = 130;
private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140;
private static final int DO_CLOSE_CONNECTION = 150;
+ private static final int DO_COMMIT_CONTENT = 160;
@GuardedBy("mLock")
@Nullable
@@ -241,6 +243,11 @@
dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION));
}
+ public void commitContent(InputContentInfo inputContentInfo, Bundle opts,
+ int seq, IInputContextCallback callback) {
+ dispatchMessage(obtainMessageOOSC(DO_COMMIT_CONTENT, inputContentInfo, opts, seq, callback));
+ }
+
void dispatchMessage(Message msg) {
// If we are calling this from the main thread, then we can call
// right through. Otherwise, we need to send the message to the
@@ -552,6 +559,29 @@
}
return;
}
+ case DO_COMMIT_CONTENT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ InputConnection ic = getInputConnection();
+ if (ic == null || !isActive()) {
+ Log.w(TAG, "commitContent on inactive InputConnection");
+ args.callback.setCommitContentResult(false, args.seq);
+ return;
+ }
+ final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1;
+ if (inputContentInfo == null || !inputContentInfo.validate()) {
+ Log.w(TAG, "commitContent with invalid inputContentInfo="
+ + inputContentInfo);
+ args.callback.setCommitContentResult(false, args.seq);
+ return;
+ }
+ args.callback.setCommitContentResult(
+ ic.commitContent(inputContentInfo, (Bundle) args.arg2), args.seq);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Got RemoteException calling commitContent", e);
+ }
+ return;
+ }
}
Log.w(TAG, "Unhandled message code: " + msg.what);
}
@@ -582,9 +612,11 @@
return mH.obtainMessage(what, arg1, arg2, args);
}
- Message obtainMessageOSC(int what, Object arg1, int seq, IInputContextCallback callback) {
+ Message obtainMessageOOSC(int what, Object arg1, Object arg2, int seq,
+ IInputContextCallback callback) {
SomeArgs args = new SomeArgs();
args.arg1 = arg1;
+ args.arg2 = arg2;
args.callback = callback;
args.seq = seq;
return mH.obtainMessage(what, 0, 0, args);
diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl
index f7ec420..0d5c8a1 100644
--- a/core/java/com/android/internal/view/IInputContext.aidl
+++ b/core/java/com/android/internal/view/IInputContext.aidl
@@ -21,6 +21,7 @@
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputContentInfo;
import com.android.internal.view.IInputContextCallback;
@@ -74,6 +75,9 @@
void getSelectedText(int flags, int seq, IInputContextCallback callback);
- void requestUpdateCursorAnchorInfo(in int cursorUpdateMode, int seq,
+ void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq,
+ IInputContextCallback callback);
+
+ void commitContent(in InputContentInfo inputContentInfo, in Bundle opts, int sec,
IInputContextCallback callback);
}
diff --git a/core/java/com/android/internal/view/IInputContextCallback.aidl b/core/java/com/android/internal/view/IInputContextCallback.aidl
index 54ea306..0f40a83 100644
--- a/core/java/com/android/internal/view/IInputContextCallback.aidl
+++ b/core/java/com/android/internal/view/IInputContextCallback.aidl
@@ -28,4 +28,5 @@
void setExtractedText(in ExtractedText extractedText, int seq);
void setSelectedText(CharSequence selectedText, int seq);
void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq);
+ void setCommitContentResult(boolean result, int seq);
}
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index f9884d8..5e78ec5 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -29,6 +29,7 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionInspector;
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
+import android.view.inputmethod.InputContentInfo;
public class InputConnectionWrapper implements InputConnection {
private static final int MAX_WAIT_TIME_MILLIS = 2000;
@@ -46,7 +47,8 @@
public ExtractedText mExtractedText;
public int mCursorCapsMode;
public boolean mRequestUpdateCursorAnchorInfoResult;
-
+ public boolean mCommitContentResult;
+
// A 'pool' of one InputContextCallback. Each ICW request will attempt to gain
// exclusive access to this object.
private static InputContextCallback sInstance = new InputContextCallback();
@@ -172,6 +174,19 @@
}
}
+ public void setCommitContentResult(boolean result, int seq) {
+ synchronized (this) {
+ if (seq == mSeq) {
+ mCommitContentResult = result;
+ mHaveValue = true;
+ notifyAll();
+ } else {
+ Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ + ") in setCommitContentResult, ignoring.");
+ }
+ }
+ }
+
/**
* Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
*
@@ -491,6 +506,28 @@
// Nothing should happen when called from input method.
}
+ public boolean commitContent(InputContentInfo inputContentInfo, Bundle opts) {
+ boolean result = false;
+ if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) {
+ // This method is not implemented.
+ return false;
+ }
+ try {
+ InputContextCallback callback = InputContextCallback.getInstance();
+ mIInputContext.commitContent(inputContentInfo, opts, callback.mSeq, callback);
+ synchronized (callback) {
+ callback.waitForResultLocked();
+ if (callback.mHaveValue) {
+ result = callback.mCommitContentResult;
+ }
+ }
+ callback.dispose();
+ } catch (RemoteException e) {
+ return false;
+ }
+ return result;
+ }
+
private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
return (mMissingMethods & methodFlag) == methodFlag;
}
diff --git a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
index 9e2cbdb..d28ab07 100644
--- a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
+++ b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
@@ -30,6 +30,8 @@
@HasNativeInterpolator
public class FallbackLUTInterpolator implements NativeInterpolatorFactory, TimeInterpolator {
+ // If the duration of an animation is more than 300 frames, we cap the sample size to 300.
+ private static final int MAX_SAMPLE_POINTS = 300;
private TimeInterpolator mSourceInterpolator;
private final float mLut[];
@@ -47,6 +49,7 @@
int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
// We need 2 frame values as the minimal.
int numAnimFrames = Math.max(2, (int) Math.ceil(((double) duration) / animIntervalMs));
+ numAnimFrames = Math.min(numAnimFrames, MAX_SAMPLE_POINTS);
float values[] = new float[numAnimFrames];
float lastFrame = numAnimFrames - 1;
for (int i = 0; i < numAnimFrames; i++) {
diff --git a/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp b/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp
index 4b2a72d..47252ad 100644
--- a/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp
+++ b/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp
@@ -95,6 +95,12 @@
return reinterpret_cast<jlong>(animatorSet);
}
+static void setVectorDrawableTarget(JNIEnv*, jobject,jlong animatorPtr, jlong vectorDrawablePtr) {
+ VectorDrawable::Tree* tree = reinterpret_cast<VectorDrawable::Tree*>(vectorDrawablePtr);
+ PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorPtr);
+ set->setVectorDrawable(tree);
+}
+
static jlong createGroupPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jint propertyId,
jfloat startValue, jfloat endValue) {
VectorDrawable::Group* group = reinterpret_cast<VectorDrawable::Group*>(nativePtr);
@@ -136,14 +142,24 @@
startValue, endValue);
return reinterpret_cast<jlong>(newHolder);
}
-static void setPropertyHolderData(JNIEnv* env, jobject, jlong propertyHolderPtr,
+static void setFloatPropertyHolderData(JNIEnv* env, jobject, jlong propertyHolderPtr,
jfloatArray srcData, jint length) {
-
jfloat* propertyData = env->GetFloatArrayElements(srcData, nullptr);
- PropertyValuesHolder* holder = reinterpret_cast<PropertyValuesHolder*>(propertyHolderPtr);
+ PropertyValuesHolderImpl<float>* holder =
+ reinterpret_cast<PropertyValuesHolderImpl<float>*>(propertyHolderPtr);
holder->setPropertyDataSource(propertyData, length);
env->ReleaseFloatArrayElements(srcData, propertyData, JNI_ABORT);
}
+
+static void setIntPropertyHolderData(JNIEnv* env, jobject, jlong propertyHolderPtr,
+ jintArray srcData, jint length) {
+ jint* propertyData = env->GetIntArrayElements(srcData, nullptr);
+ PropertyValuesHolderImpl<int>* holder =
+ reinterpret_cast<PropertyValuesHolderImpl<int>*>(propertyHolderPtr);
+ holder->setPropertyDataSource(propertyData, length);
+ env->ReleaseIntArrayElements(srcData, propertyData, JNI_ABORT);
+}
+
static void start(JNIEnv* env, jobject, jlong animatorSetPtr, jobject finishListener, jint id) {
PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorSetPtr);
AnimationListener* listener = createAnimationListener(env, finishListener, id);
@@ -168,13 +184,15 @@
static const JNINativeMethod gMethods[] = {
{"nCreateAnimatorSet", "()J", (void*)createAnimatorSet},
+ {"nSetVectorDrawableTarget", "(JJ)V", (void*)setVectorDrawableTarget},
{"nAddAnimator", "(JJJJJI)V", (void*)addAnimator},
{"nCreateGroupPropertyHolder", "!(JIFF)J", (void*)createGroupPropertyHolder},
{"nCreatePathDataPropertyHolder", "!(JJJ)J", (void*)createPathDataPropertyHolder},
{"nCreatePathColorPropertyHolder", "!(JIII)J", (void*)createPathColorPropertyHolder},
{"nCreatePathPropertyHolder", "!(JIFF)J", (void*)createPathPropertyHolder},
{"nCreateRootAlphaPropertyHolder", "!(JFF)J", (void*)createRootAlphaPropertyHolder},
- {"nSetPropertyHolderData", "(J[FI)V", (void*)setPropertyHolderData},
+ {"nSetPropertyHolderData", "(J[FI)V", (void*)setFloatPropertyHolderData},
+ {"nSetPropertyHolderData", "(J[II)V", (void*)setIntPropertyHolderData},
{"nStart", "(JLandroid/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimatorRT;I)V", (void*)start},
{"nReverse", "(JLandroid/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimatorRT;I)V", (void*)reverse},
{"nEnd", "!(J)V", (void*)end},
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 172e12d..2dcf8aa 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -32,6 +32,7 @@
#include <utils/Looper.h>
#include <utils/RefBase.h>
#include <utils/StrongPointer.h>
+#include <utils/Timers.h>
#include <android_runtime/android_view_Surface.h>
#include <system/window.h>
@@ -44,6 +45,7 @@
#include <FrameMetricsObserver.h>
#include <IContextFactory.h>
#include <JankTracker.h>
+#include <PropertyValuesAnimatorSet.h>
#include <RenderNode.h>
#include <renderthread/CanvasContext.h>
#include <renderthread/RenderProxy.h>
@@ -122,6 +124,31 @@
std::vector<OnFinishedEvent> mOnFinishedEvents;
};
+class FinishAndInvokeListener : public MessageHandler {
+public:
+ explicit FinishAndInvokeListener(PropertyValuesAnimatorSet* anim)
+ : mAnimator(anim) {
+ mListener = anim->getOneShotListener();
+ mRequestId = anim->getRequestId();
+ }
+
+ virtual void handleMessage(const Message& message) {
+ if (mAnimator->getRequestId() == mRequestId) {
+ // Request Id has not changed, meaning there's no animation lifecyle change since the
+ // message is posted, so go ahead and call finish to make sure the PlayState is properly
+ // updated. This is needed because before the next frame comes in from UI thread to
+ // trigger an animation update, there could be reverse/cancel etc. So we need to update
+ // the playstate in time to ensure all the subsequent events get chained properly.
+ mAnimator->end();
+ }
+ mListener->onAnimationFinished(nullptr);
+ }
+private:
+ sp<PropertyValuesAnimatorSet> mAnimator;
+ sp<AnimationListener> mListener;
+ uint32_t mRequestId;
+};
+
class RenderingException : public MessageHandler {
public:
RenderingException(JavaVM* vm, const std::string& message)
@@ -160,6 +187,15 @@
virtual void prepareTree(TreeInfo& info) override {
info.errorHandler = this;
+
+ for (auto& anim : mVectorDrawableAnimators) {
+ // Assume that the property change in VD from the animators will not be consumed. Mark
+ // otherwise if the VDs are found in the display list tree. For VDs that are not in
+ // the display list tree, we stop providing animation pulses by 1) removing them from
+ // the animation list, 2) post a delayed message to end them at end time so their
+ // listeners can receive the corresponding callbacks.
+ anim->getVectorDrawable()->setPropertyChangeWillBeConsumed(false);
+ }
// TODO: This is hacky
info.windowInsetLeft = -stagingProperties().getLeft();
info.windowInsetTop = -stagingProperties().getTop();
@@ -169,16 +205,46 @@
info.windowInsetLeft = 0;
info.windowInsetTop = 0;
info.errorHandler = nullptr;
+
+ for (auto it = mVectorDrawableAnimators.begin(); it != mVectorDrawableAnimators.end();) {
+ if (!(*it)->getVectorDrawable()->getPropertyChangeWillBeConsumed()) {
+ // Vector Drawable is not in the display list, we should remove this animator from
+ // the list and post a delayed message to end the animator.
+ detachVectorDrawableAnimator(it->get());
+ it = mVectorDrawableAnimators.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ info.out.hasAnimations |= !mVectorDrawableAnimators.empty();
}
void sendMessage(const sp<MessageHandler>& handler) {
mLooper->sendMessage(handler, 0);
}
+ void sendMessageDelayed(const sp<MessageHandler>& handler, nsecs_t delayInMs) {
+ mLooper->sendMessageDelayed(ms2ns(delayInMs), handler, 0);
+ }
+
void attachAnimatingNode(RenderNode* animatingNode) {
mPendingAnimatingRenderNodes.push_back(animatingNode);
}
+ void attachPendingVectorDrawableAnimators() {
+ mVectorDrawableAnimators.insert(mPendingVectorDrawableAnimators.begin(),
+ mPendingVectorDrawableAnimators.end());
+ mPendingVectorDrawableAnimators.clear();
+ }
+
+ void detachAnimators() {
+ // Remove animators from the list and post a delayed message in future to end the animator
+ for (auto& anim : mVectorDrawableAnimators) {
+ detachVectorDrawableAnimator(anim.get());
+ }
+ mVectorDrawableAnimators.clear();
+ }
+
void doAttachAnimatingNodes(AnimationContext* context) {
for (size_t i = 0; i < mPendingAnimatingRenderNodes.size(); i++) {
RenderNode* node = mPendingAnimatingRenderNodes[i].get();
@@ -187,17 +253,57 @@
mPendingAnimatingRenderNodes.clear();
}
+ void runVectorDrawableAnimators(AnimationContext* context) {
+ for (auto it = mVectorDrawableAnimators.begin(); it != mVectorDrawableAnimators.end();) {
+ (*it)->pushStaging(*context);
+ if ((*it)->animate(*context)) {
+ it = mVectorDrawableAnimators.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ }
+
void destroy() {
for (auto& renderNode : mPendingAnimatingRenderNodes) {
renderNode->animators().endAllStagingAnimators();
}
mPendingAnimatingRenderNodes.clear();
+ mPendingVectorDrawableAnimators.clear();
+ }
+
+ void addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim) {
+ mPendingVectorDrawableAnimators.insert(anim);
}
private:
sp<Looper> mLooper;
JavaVM* mVm;
std::vector< sp<RenderNode> > mPendingAnimatingRenderNodes;
+ std::set< sp<PropertyValuesAnimatorSet> > mPendingVectorDrawableAnimators;
+ std::set< sp<PropertyValuesAnimatorSet> > mVectorDrawableAnimators;
+ void detachVectorDrawableAnimator(PropertyValuesAnimatorSet* anim) {
+ if (anim->isInfinite() || !anim->isRunning()) {
+ // Do not need to post anything if the animation is infinite (i.e. no meaningful
+ // end listener action), or if the animation has already ended.
+ return;
+ }
+ nsecs_t remainingTimeInMs = anim->getRemainingPlayTime();
+ // Post a delayed onFinished event that is scheduled to be handled when the animator ends.
+ if (anim->getOneShotListener()) {
+ // VectorDrawable's oneshot listener is updated when there are user triggered animation
+ // lifecycle changes, such as start(), end(), etc. By using checking and clearing
+ // one shot listener, we ensure the same end listener event gets posted only once.
+ // Therefore no duplicates. Another benefit of using one shot listener is that no
+ // removal is necessary: the end time of animation will not change unless triggered by
+ // user events, in which case the already posted listener's id will become stale, and
+ // the onFinished callback will then be ignored.
+ sp<FinishAndInvokeListener> message
+ = new FinishAndInvokeListener(anim);
+ sendMessageDelayed(message, remainingTimeInMs);
+ anim->clearOneShotListener();
+ }
+ }
};
class AnimationContextBridge : public AnimationContext {
@@ -213,8 +319,16 @@
virtual void startFrame(TreeInfo::TraversalMode mode) {
if (mode == TreeInfo::MODE_FULL) {
mRootNode->doAttachAnimatingNodes(this);
+ mRootNode->attachPendingVectorDrawableAnimators();
}
AnimationContext::startFrame(mode);
+ // Run VectorDrawable animators in the beginning of the frame instead of during prepareTree,
+ // because one VD can be in multiple render nodes' display list. So it's more simple to
+ // run them all at once before prepareTree than running them or checking whether they have
+ // already ran in each RenderNode. Note that these animators don't damage the RenderNodes.
+ // The damaging is done in prepareTree as needed after checking whether a VD has been
+ // modified.
+ mRootNode->runVectorDrawableAnimators(this);
}
// Runs any animations still left in mCurrentFrameAnimations
@@ -223,6 +337,10 @@
postOnFinishedEvents();
}
+ virtual void detachAnimators() override {
+ mRootNode->detachAnimators();
+ }
+
virtual void callOnFinished(BaseRenderNodeAnimator* animator, AnimationListener* listener) {
OnFinishedEvent event(animator, listener);
mOnFinishedEvents.push_back(event);
@@ -230,6 +348,7 @@
virtual void destroy() {
AnimationContext::destroy();
+ detachAnimators();
postOnFinishedEvents();
}
@@ -527,6 +646,13 @@
rootRenderNode->attachAnimatingNode(animatingNode);
}
+static void android_view_ThreadedRenderer_registerVectorDrawableAnimator(JNIEnv* env, jobject clazz,
+ jlong rootNodePtr, jlong animatorPtr) {
+ RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootNodePtr);
+ PropertyValuesAnimatorSet* animator = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorPtr);
+ rootRenderNode->addVectorDrawableAnimator(animator);
+}
+
static void android_view_ThreadedRenderer_invokeFunctor(JNIEnv* env, jobject clazz,
jlong functorPtr, jboolean waitForCompletion) {
Functor* functor = reinterpret_cast<Functor*>(functorPtr);
@@ -738,6 +864,7 @@
{ "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
{ "nDestroy", "(JJ)V", (void*) android_view_ThreadedRenderer_destroy },
{ "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode },
+ { "nRegisterVectorDrawableAnimator", "(JJ)V", (void*) android_view_ThreadedRenderer_registerVectorDrawableAnimator },
{ "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor },
{ "nCreateTextureLayer", "(J)J", (void*) android_view_ThreadedRenderer_createTextureLayer },
{ "nBuildLayer", "(JJ)V", (void*) android_view_ThreadedRenderer_buildLayer },
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e9a3409..3783dc8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -469,12 +469,6 @@
<protected-broadcast android:name="android.net.wifi.PASSPOINT_ICON_RECEIVED" />
<protected-broadcast android:name="com.android.server.notification.CountdownConditionProvider" />
- <protected-broadcast android:name="com.android.ims.IMS_SERVICE_UP" />
- <protected-broadcast android:name="com.android.ims.IMS_INCOMING_CALL" />
- <protected-broadcast android:name="com.android.ims.internal.uce.UCE_SERVICE_UP" />
- <protected-broadcast android:name="com.android.intent.action.IMS_FEATURE_CHANGED" />
- <protected-broadcast android:name="com.android.intent.action.IMS_CONFIG_CHANGED" />
-
<protected-broadcast android:name="com.android.internal.location.ALARM_WAKEUP" />
<protected-broadcast android:name="com.android.internal.location.ALARM_TIMEOUT" />
<protected-broadcast android:name="android.intent.action.GLOBAL_BUTTON" />
@@ -483,6 +477,8 @@
<protected-broadcast android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
<protected-broadcast android:name="com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK" />
+ <protected-broadcast android:name="com.android.server.am.ACTION_RESET_DEMO" />
+
<!-- ====================================================================== -->
<!-- RUNTIME PERMISSIONS -->
<!-- ====================================================================== -->
diff --git a/core/res/res/drawable-watch/dialog_background_material.xml b/core/res/res/drawable-watch/dialog_background_material.xml
new file mode 100644
index 0000000..de52f08
--- /dev/null
+++ b/core/res/res/drawable-watch/dialog_background_material.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="?attr/colorBackground" />
+</shape>
diff --git a/core/res/res/layout-notround-watch/alert_dialog_header_micro.xml b/core/res/res/layout-notround-watch/alert_dialog_header_micro.xml
new file mode 100644
index 0000000..fc840d9
--- /dev/null
+++ b/core/res/res/layout-notround-watch/alert_dialog_header_micro.xml
@@ -0,0 +1,41 @@
+<?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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="top|center_horizontal"
+ android:minHeight="@dimen/alert_dialog_title_height">
+ <ImageView android:id="@+id/icon"
+ android:maxHeight="24dp"
+ android:maxWidth="24dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@null" />
+ <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
+ style="?android:attr/windowTitleStyle"
+ android:ellipsize="end"
+ android:layout_marginStart="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart" />
+</LinearLayout>
diff --git a/core/res/res/layout-round-watch/alert_dialog_header_micro.xml b/core/res/res/layout-round-watch/alert_dialog_header_micro.xml
new file mode 100644
index 0000000..6f7ae02
--- /dev/null
+++ b/core/res/res/layout-round-watch/alert_dialog_header_micro.xml
@@ -0,0 +1,38 @@
+<?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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="top|center_horizontal"
+ android:minHeight="@dimen/alert_dialog_title_height">
+ <ImageView android:id="@+id/icon"
+ android:maxHeight="24dp"
+ android:maxWidth="24dp"
+ android:layout_marginTop="12dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@null" />
+ <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
+ style="?android:attr/windowTitleStyle"
+ android:ellipsize="end"
+ android:layout_marginTop="36dp"
+ android:layout_marginBottom="4dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center" />
+</FrameLayout>
diff --git a/core/res/res/layout-watch/alert_dialog_material.xml b/core/res/res/layout-watch/alert_dialog_material.xml
new file mode 100644
index 0000000..e627d42
--- /dev/null
+++ b/core/res/res/layout-watch/alert_dialog_material.xml
@@ -0,0 +1,113 @@
+<?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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ScrollView
+ android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <!-- Top Panel -->
+ <FrameLayout
+ android:paddingLeft="?dialogPreferredPadding"
+ android:paddingRight="?dialogPreferredPadding"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/topPanel">
+ <include android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ layout="@layout/alert_dialog_header_micro"/>
+ </FrameLayout>
+
+ <!-- Content Panel -->
+ <FrameLayout android:id="@+id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false">
+ <TextView android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Material.Body1"
+ android:paddingStart="?dialogPreferredPadding"
+ android:paddingEnd="?dialogPreferredPadding"
+ android:paddingTop="8dip"
+ android:paddingBottom="8dip"/>
+ </FrameLayout>
+
+ <!-- Custom Panel, to replace content panel if needed -->
+ <FrameLayout android:id="@+id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:minHeight="64dp">
+ <FrameLayout android:id="@+android:id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+
+ <!-- Button Panel -->
+ <FrameLayout
+ android:id="@+id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="?android:attr/dividerHorizontal"
+ android:showDividers="beginning"
+ android:dividerPadding="0dip"
+ android:orientation="vertical"
+ android:minHeight="@dimen/alert_dialog_button_bar_height"
+ android:paddingBottom="?dialogPreferredPadding"
+ style="?android:attr/buttonBarStyle"
+ android:layoutDirection="locale"
+ android:measureWithLargestChild="true">
+ <Button android:id="@+id/button1"
+ android:layout_gravity="start"
+ android:layout_weight="1"
+ android:layout_marginLeft="?dialogPreferredPadding"
+ android:layout_marginRight="?dialogPreferredPadding"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <Button android:id="@+id/button3"
+ android:layout_gravity="start"
+ android:layout_weight="1"
+ android:layout_marginLeft="?dialogPreferredPadding"
+ android:layout_marginRight="?dialogPreferredPadding"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <Button android:id="@+id/button2"
+ android:layout_gravity="start"
+ android:layout_weight="1"
+ android:layout_marginLeft="?dialogPreferredPadding"
+ android:layout_marginRight="?dialogPreferredPadding"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+ </FrameLayout>
+ </LinearLayout>
+ </ScrollView>
+</FrameLayout>
diff --git a/core/res/res/layout-watch/input_method_extract_view.xml b/core/res/res/layout-watch/input_method_extract_view.xml
index e3cd2ce..038b766 100644
--- a/core/res/res/layout-watch/input_method_extract_view.xml
+++ b/core/res/res/layout-watch/input_method_extract_view.xml
@@ -31,10 +31,10 @@
android:inputType="text"
android:layout_weight="1"
android:fontFamily="sans-serif-condensed-light"
- android:textColor="@color/primary_text_default_material_dark"
+ android:textColor="@color/primary_text_material_dark"
+ android:textColorHint="@color/secondary_text_material_dark"
android:textColorHighlight="@color/accent_material_dark"
android:textSize="18dp"
- android:cursorVisible="false"
android:gravity="bottom|right"
/>
diff --git a/core/res/res/layout/number_picker_with_selector_wheel_micro.xml b/core/res/res/layout-watch/number_picker_material.xml
similarity index 100%
rename from core/res/res/layout/number_picker_with_selector_wheel_micro.xml
rename to core/res/res/layout-watch/number_picker_material.xml
diff --git a/core/res/res/layout-watch/preference_material.xml b/core/res/res/layout-watch/preference_material.xml
new file mode 100644
index 0000000..5da64fc
--- /dev/null
+++ b/core/res/res/layout-watch/preference_material.xml
@@ -0,0 +1,82 @@
+<?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.
+-->
+
+<!-- Layout for a Preference in a PreferenceActivity. The
+ Preference is able to place a specific widget for its particular
+ type in the "widget_frame" layout. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/listPreferredItemHeightSmall"
+ android:gravity="center_vertical"
+ android:paddingStart="?attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?attr/listPreferredItemPaddingEnd"
+ android:background="?attr/activatedBackgroundIndicator"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="-4dp"
+ android:minWidth="32dp"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal"
+ android:paddingEnd="8dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+ <com.android.internal.widget.PreferenceImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxWidth="24dp"
+ android:maxHeight="24dp" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:textAppearance="?attr/textAppearanceListItem"
+ android:ellipsize="end" />
+
+ <TextView android:id="@+id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignStart="@id/title"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ android:textColor="?attr/textColorSecondary"
+ android:maxLines="10" />
+
+ </RelativeLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout android:id="@+id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="end|center_vertical"
+ android:paddingStart="4dp"
+ android:orientation="vertical" />
+
+</LinearLayout>
diff --git a/core/res/res/layout/alert_dialog_micro.xml b/core/res/res/layout/alert_dialog_micro.xml
deleted file mode 100644
index abdbd16..0000000
--- a/core/res/res/layout/alert_dialog_micro.xml
+++ /dev/null
@@ -1,140 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2014 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
- -->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/parentPanel"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@android:color/white"
- android:layout_gravity="center"
- android:orientation="vertical">
-
- <LinearLayout android:id="@+id/topPanel"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <View android:id="@+id/titleDividerTop"
- android:layout_width="match_parent"
- android:layout_height="2dip"
- android:visibility="gone"
- android:background="@android:color/holo_blue_light" />
- <LinearLayout android:id="@+id/title_template"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:gravity="center_vertical|start"
- android:minHeight="@dimen/alert_dialog_title_height"
- android:layout_marginStart="16dip"
- android:layout_marginEnd="16dip">
- <ImageView android:id="@+id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingEnd="8dip"
- android:src="@null" />
- <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
- style="?android:attr/windowTitleStyle"
- android:singleLine="true"
- android:ellipsize="end"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAlignment="viewStart" />
- </LinearLayout>
- <View android:id="@+id/titleDivider"
- android:layout_width="match_parent"
- android:layout_height="2dip"
- android:visibility="gone"
- android:background="@android:color/holo_blue_light" />
- <!-- If the client uses a customTitle, it will be added here. -->
- </LinearLayout>
-
- <LinearLayout android:id="@+id/contentPanel"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical"
- android:minHeight="64dp">
- <ScrollView android:id="@+id/scrollView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:clipToPadding="false">
- <TextView android:id="@+id/message"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingStart="16dip"
- android:paddingEnd="16dip"
- android:paddingTop="8dip"
- android:paddingBottom="8dip"/>
- </ScrollView>
- </LinearLayout>
-
- <FrameLayout android:id="@+id/customPanel"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:minHeight="64dp">
- <FrameLayout android:id="@+android:id/custom"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
- </FrameLayout>
-
- <LinearLayout android:id="@+id/buttonPanel"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="@dimen/alert_dialog_button_bar_height"
- android:orientation="vertical"
- android:divider="?android:attr/dividerHorizontal"
- android:showDividers="beginning"
- android:dividerPadding="0dip">
- <LinearLayout
- style="?android:attr/buttonBarStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layoutDirection="locale"
- android:measureWithLargestChild="true">
- <Button android:id="@+id/button2"
- android:layout_width="wrap_content"
- android:layout_gravity="start"
- android:layout_weight="1"
- android:maxLines="2"
- style="?android:attr/buttonBarButtonStyle"
- android:textSize="14sp"
- android:minHeight="@dimen/alert_dialog_button_bar_height"
- android:layout_height="wrap_content" />
- <Button android:id="@+id/button3"
- android:layout_width="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_weight="1"
- android:maxLines="2"
- style="?android:attr/buttonBarButtonStyle"
- android:textSize="14sp"
- android:minHeight="@dimen/alert_dialog_button_bar_height"
- android:layout_height="wrap_content" />
- <Button android:id="@+id/button1"
- android:layout_width="wrap_content"
- android:layout_gravity="end"
- android:layout_weight="1"
- android:maxLines="2"
- android:minHeight="@dimen/alert_dialog_button_bar_height"
- style="?android:attr/buttonBarButtonStyle"
- android:textSize="14sp"
- android:layout_height="wrap_content" />
- </LinearLayout>
- </LinearLayout>
-</LinearLayout>
diff --git a/core/res/res/layout/immersive_mode_cling.xml b/core/res/res/layout/immersive_mode_cling.xml
index 28fbea5..b08b0f4 100644
--- a/core/res/res/layout/immersive_mode_cling.xml
+++ b/core/res/res/layout/immersive_mode_cling.xml
@@ -16,7 +16,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="#ff009688"
+ android:background="?android:attr/colorAccent"
android:gravity="center_vertical"
android:paddingBottom="24dp">
@@ -47,7 +47,7 @@
android:paddingTop="8dp"
android:scaleType="center"
android:src="@drawable/ic_expand_more_48dp"
- android:tint="#ff009688"/>
+ android:tint="?android:attr/colorAccent"/>
</FrameLayout>
<TextView
diff --git a/core/res/res/layout/preference_list_fragment.xml b/core/res/res/layout/preference_list_fragment.xml
index fc53a1a..c43975e 100644
--- a/core/res/res/layout/preference_list_fragment.xml
+++ b/core/res/res/layout/preference_list_fragment.xml
@@ -33,8 +33,6 @@
style="?attr/preferenceFragmentListStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingTop="0dip"
- android:paddingBottom="@dimen/preference_fragment_padding_bottom"
android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
android:clipToPadding="false"
android:drawSelectorOnTop="false"
diff --git a/core/res/res/layout/preference_list_fragment_material.xml b/core/res/res/layout/preference_list_fragment_material.xml
index e411c0e..db2fe7d 100644
--- a/core/res/res/layout/preference_list_fragment_material.xml
+++ b/core/res/res/layout/preference_list_fragment_material.xml
@@ -32,8 +32,6 @@
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingTop="0dip"
- android:paddingBottom="@dimen/preference_fragment_padding_bottom"
style="?attr/preferenceFragmentListStyle"
android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
android:clipToPadding="false"
diff --git a/core/res/res/values-watch/dimens.xml b/core/res/res/values-notround-watch/dimens.xml
similarity index 100%
rename from core/res/res/values-watch/dimens.xml
rename to core/res/res/values-notround-watch/dimens.xml
diff --git a/core/res/res/values-notround-watch/dimens_material.xml b/core/res/res/values-notround-watch/dimens_material.xml
new file mode 100644
index 0000000..b02437b
--- /dev/null
+++ b/core/res/res/values-notround-watch/dimens_material.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+<resources>
+ <dimen name="dialog_padding_material">8dp</dimen>
+ <dimen name="preference_fragment_padding_vertical_material">0dp</dimen>
+
+ <dimen name="list_item_padding_horizontal_material">16dp</dimen>
+ <dimen name="list_item_padding_start_material">16dp</dimen>
+ <dimen name="list_item_padding_end_material">16dp</dimen>
+</resources>
diff --git a/core/res/res/values-notround-watch/styles_material.xml b/core/res/res/values-notround-watch/styles_material.xml
new file mode 100644
index 0000000..cd8521f4
--- /dev/null
+++ b/core/res/res/values-notround-watch/styles_material.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<resources>
+ <style name="TextAppearance.Material.AlertDialogMessage" parent="TextAppearance.Material.Body1"/>
+</resources>
diff --git a/core/res/res/values-round-watch/config_material.xml b/core/res/res/values-round-watch/config_material.xml
new file mode 100644
index 0000000..bf445ef
--- /dev/null
+++ b/core/res/res/values-round-watch/config_material.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for watch products. Do not translate. -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Don't clip on round screens so the list can scroll past the rounded edges. -->
+ <bool name="config_preferenceFragmentClipToPadding">false</bool>
+</resources>
diff --git a/core/res/res/values-round-watch/dimens_material.xml b/core/res/res/values-round-watch/dimens_material.xml
new file mode 100644
index 0000000..a417d19
--- /dev/null
+++ b/core/res/res/values-round-watch/dimens_material.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+<resources>
+ <dimen name="dialog_padding_material">32dp</dimen>
+ <dimen name="preference_fragment_padding_vertical_material">22dp</dimen>
+
+ <dimen name="list_item_padding_horizontal_material">32dp</dimen>
+ <dimen name="list_item_padding_start_material">40dp</dimen>
+ <dimen name="list_item_padding_end_material">24dp</dimen>
+</resources>
diff --git a/core/res/res/values-round-watch/styles_material.xml b/core/res/res/values-round-watch/styles_material.xml
new file mode 100644
index 0000000..a2f3c02
--- /dev/null
+++ b/core/res/res/values-round-watch/styles_material.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<resources>
+ <style name="TextAppearance.Material.AlertDialogMessage" parent="TextAppearance.Material.Body1">
+ <item name="textAlignment">center</item>
+ </style>
+</resources>
diff --git a/core/res/res/values-watch/colors_material.xml b/core/res/res/values-watch/colors_material.xml
new file mode 100644
index 0000000..cbdf879
--- /dev/null
+++ b/core/res/res/values-watch/colors_material.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<resources>
+ <color name="background_material_dark">@color/micro_blue_grey_b15</color>
+ <color name="background_floating_material_dark">@color/micro_blue_grey_b30</color>
+
+ <color name="primary_material_dark">@color/micro_blue_grey_b65</color>
+ <color name="primary_dark_material_dark">@color/micro_blue_grey_b40</color>
+
+ <color name="accent_material_dark">@color/micro_blue_grey_b100</color>
+ <color name="accent_material_light">@color/micro_blue_grey_500</color>
+
+ <!-- Primary & accent colors -->
+ <eat-comment />
+
+ <!-- App color -->
+ <color name="micro_blue_grey_500">#ff607d8b</color>
+ <!-- Accent -->
+ <color name="micro_blue_grey_b100">#ffb0e5ff</color>
+ <!-- Lighter UI element -->
+ <color name="micro_blue_grey_b65">#ff7295a6</color>
+ <!-- UI Element -->
+ <color name="micro_blue_grey_b40">#ff465b66</color>
+ <!-- Lighter background -->
+ <color name="micro_blue_grey_b30">#ff35454d</color>
+ <!-- Dark background -->
+ <color name="micro_blue_grey_b15">#ff1a2226</color>
+
+ <!-- Button colors -->
+ <eat-comment />
+
+ <color name="micro_confirm_green">#ff4fc0b0</color>
+ <color name="micro_button_gray">#ffc2c2c2</color>
+ <color name="micro_action_blue">#ff0288d1</color>
+</resources>
diff --git a/core/res/res/values-watch/config_material.xml b/core/res/res/values-watch/config_material.xml
new file mode 100644
index 0000000..81b53e7
--- /dev/null
+++ b/core/res/res/values-watch/config_material.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for watch products. Do not translate. -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Watch type devices have limited screen real-estate, and thus action bars should not be
+ used. -->
+ <bool name="config_windowActionBarSupported">false</bool>
+
+ <!-- Watch type devices have limited screen real-estate, and thus titles should not be used. -->
+ <bool name="config_windowNoTitleDefault">true</bool>
+
+ <!-- Use micro alert controller -->
+ <integer name="config_alertDialogController">1</integer>
+
+ <!-- Always overscan by default to ensure onApplyWindowInsets will always be called. -->
+ <bool name="config_windowOverscanByDefault">true</bool>
+
+ <!-- Due to the smaller screen size, have dialog titles occupy more than 1 line. -->
+ <integer name="config_dialogWindowTitleMaxLines">3</integer>
+</resources>
diff --git a/core/res/res/values-watch/dimens_material.xml b/core/res/res/values-watch/dimens_material.xml
new file mode 100644
index 0000000..d579434
--- /dev/null
+++ b/core/res/res/values-watch/dimens_material.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<resources>
+ <item name="text_line_spacing_multiplier_material" format="float" type="dimen">1.2</item>
+</resources>
diff --git a/core/res/res/values-watch/donottranslate_material.xml b/core/res/res/values-watch/donottranslate_material.xml
new file mode 100644
index 0000000..a6f2ff4
--- /dev/null
+++ b/core/res/res/values-watch/donottranslate_material.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.
+-->
+
+<resources>
+ <string name="font_family_display_4_material">sans-serif-condensed-light</string>
+ <string name="font_family_display_3_material">sans-serif-condensed-light</string>
+ <string name="font_family_display_2_material">sans-serif-condensed-light</string>
+ <string name="font_family_display_1_material">sans-serif-condensed-light</string>
+ <string name="font_family_headline_material">sans-serif-condensed-light</string>
+ <string name="font_family_title_material">sans-serif-condensed</string>
+ <string name="font_family_subhead_material">sans-serif-condensed-light</string>
+ <string name="font_family_menu_material">sans-serif-condensed-light</string>
+ <string name="font_family_body_2_material">sans-serif-condensed</string>
+ <string name="font_family_body_1_material">sans-serif-condensed-light</string>
+ <string name="font_family_caption_material">sans-serif-condensed-light</string>
+ <string name="font_family_button_material">sans-serif-condensed</string>
+ </resources>
diff --git a/core/res/res/values-watch/styles_material.xml b/core/res/res/values-watch/styles_material.xml
new file mode 100644
index 0000000..c19cc72
--- /dev/null
+++ b/core/res/res/values-watch/styles_material.xml
@@ -0,0 +1,83 @@
+<?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.
+-->
+
+<!--
+===============================================================
+ PLEASE READ
+===============================================================
+
+The Material themes must not be modified in order to pass CTS.
+Many related themes and styles depend on other values defined in this file.
+If you would like to provide custom themes and styles for your device,
+please see styles_device_defaults.xml.
+
+===============================================================
+ PLEASE READ
+===============================================================
+ -->
+<resources>
+ <style name="Animation.Material.Activity" parent="Animation.Activity">
+ <item name="activityOpenEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="activityOpenRemoteViewsEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="activityOpenExitAnimation">@anim/slide_in_exit_micro</item>
+ <item name="activityCloseEnterAnimation">@null</item>
+ <item name="activityCloseExitAnimation">@anim/slide_out_micro</item>
+ <item name="taskOpenEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="taskOpenExitAnimation">@anim/slide_in_exit_micro</item>
+ <item name="taskCloseEnterAnimation">@null</item>
+ <item name="taskCloseExitAnimation">@anim/slide_out_micro</item>
+ <item name="taskToFrontEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="taskToFrontExitAnimation">@anim/slide_in_exit_micro</item>
+ <item name="taskToBackEnterAnimation">@null</item>
+ <item name="taskToBackExitAnimation">@anim/slide_out_micro</item>
+ <item name="wallpaperOpenEnterAnimation">@null</item>
+ <item name="wallpaperOpenExitAnimation">@anim/slide_out_micro</item>
+ <item name="wallpaperCloseEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="wallpaperCloseExitAnimation">@anim/slide_in_exit_micro</item>
+ <item name="wallpaperIntraOpenEnterAnimation">@null</item>
+ <item name="wallpaperIntraOpenExitAnimation">@anim/slide_out_micro</item>
+ <item name="wallpaperIntraCloseEnterAnimation">@anim/slide_in_enter_micro</item>
+ <item name="wallpaperIntraCloseExitAnimation">@anim/slide_in_exit_micro</item>
+ </style>
+
+ <style name="Widget.Material.TextView" parent="Widget.TextView">
+ <item name="breakStrategy">balanced</item>
+ </style>
+
+ <!-- Alert dialog button bar button -->
+ <style name="Widget.Material.Button.ButtonBar.AlertDialog" parent="Widget.Material.Button.Borderless.Small">
+ <item name="gravity">center_vertical|left</item>
+ <item name="minWidth">64dp</item>
+ <item name="minHeight">@dimen/alert_dialog_button_bar_height</item>
+ </style>
+
+ <style name="Widget.Material.NumberPicker" parent="Widget.NumberPicker">
+ <item name="internalLayout">@layout/number_picker_material</item>
+ <item name="solidColor">@color/transparent</item>
+ <item name="selectionDivider">@drawable/numberpicker_selection_divider</item>
+ <item name="selectionDividerHeight">2dp</item>
+ <item name="selectionDividersDistance">48dp</item>
+ <item name="internalMinWidth">64dp</item>
+ <item name="internalMaxHeight">180dp</item>
+ <item name="virtualButtonPressedDrawable">?selectableItemBackground</item>
+ <item name="descendantFocusability">blocksDescendants</item>
+ </style>
+
+ <!-- DO NOTE TRANSLATE Spans within this text are applied to style composing regions
+ within an EditText widget. The text content is ignored and not used.
+ Note: This is @color/material_deep_teal_200, cannot use @color references here. -->
+ <string name="candidates_style"><font color="#80cbc4">candidates</font></string>
+</resources>
diff --git a/core/res/res/values-watch/themes.xml b/core/res/res/values-watch/themes.xml
index 6d6065f..04df180 100644
--- a/core/res/res/values-watch/themes.xml
+++ b/core/res/res/values-watch/themes.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2014 The Android Open Source Project
+<?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.
@@ -14,11 +14,10 @@
limitations under the License.
-->
<resources>
- <style name="Theme.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.Dialog.AppError" parent="Theme.Micro.Dialog.AppError" />
- <style name="Theme.Holo.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.Holo.Light.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.InputMethod" parent="Theme.Micro.InputMethod" />
- <style name="Theme.Material.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.Material.Light.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
+ <!-- Theme for the dialog shown when an app crashes or ANRs. Override to make it dark. -->
+ <style name="Theme.Dialog.AppError" parent="Theme.DeviceDefault.Dialog.Alert">
+ <item name="windowContentTransitions">false</item>
+ <item name="windowActivityTransitions">false</item>
+ <item name="windowCloseOnTouchOutside">false</item>
+ </style>
</resources>
diff --git a/core/res/res/values-watch/themes_device_defaults.xml b/core/res/res/values-watch/themes_device_defaults.xml
index 66509fb..2313b26 100644
--- a/core/res/res/values-watch/themes_device_defaults.xml
+++ b/core/res/res/values-watch/themes_device_defaults.xml
@@ -13,22 +13,31 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <style name="Theme.DeviceDefault" parent="Theme.Micro" />
- <style name="Theme.DeviceDefault.NoActionBar" parent="Theme.Micro" />
- <style name="Theme.DeviceDefault.Dialog" parent="Theme.Micro.Dialog" />
- <style name="Theme.DeviceDefault.DialogWhenLarge" parent="Theme.Micro.Dialog" />
- <style name="Theme.DeviceDefault.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.DeviceDefault.InputMethod" parent="Theme.Micro.InputMethod" />
- <style name="Theme.DeviceDefault.Panel" parent="Theme.Micro.Panel" />
- <style name="Theme.DeviceDefault.Light" parent="Theme.Micro.Light" />
- <style name="Theme.DeviceDefault.Light.NoActionBar" parent="Theme.Micro.Light" />
- <style name="Theme.DeviceDefault.Light.DarkActionBar" parent="Theme.Micro.Light" />
- <style name="Theme.DeviceDefault.Light.Dialog" parent="Theme.Micro.Dialog" />
- <style name="Theme.DeviceDefault.Light.DialogWhenLarge" parent="Theme.Micro.Dialog" />
- <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="Theme.Micro.Dialog.Alert" />
- <style name="Theme.DeviceDefault.Light.Panel" parent="Theme.Micro.Light.Panel" />
- <style name="Theme.DeviceDefault.Settings" parent="Theme.Micro" />
- <style name="Theme.DeviceDefault.Wallpaper" parent="Theme.Micro" />
-</resources>
+<!--
+===============================================================
+ PLEASE READ
+===============================================================
+This file contains the themes that are the Device Defaults.
+If you want to edit themes to skin your device, do it here.
+We recommend that you do not edit themes.xml and instead edit
+this file.
+
+Editing this file instead of themes.xml will greatly simplify
+merges for future platform versions and CTS compliance will be
+easier.
+===============================================================
+ PLEASE READ
+===============================================================
+ -->
+<resources>
+ <!-- Theme used for the intent picker activity. -->
+ <style name="Theme.DeviceDefault.Resolver" parent="Theme.Material">
+ <item name="colorControlActivated">?attr/colorControlHighlight</item>
+ <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
+ </style>
+
+ <!-- Use a dark theme for watches. -->
+ <style name="Theme.DeviceDefault.System" parent="Theme.Material" />
+</resources>
diff --git a/core/res/res/values-watch/themes_material.xml b/core/res/res/values-watch/themes_material.xml
new file mode 100644
index 0000000..4ae4367
--- /dev/null
+++ b/core/res/res/values-watch/themes_material.xml
@@ -0,0 +1,62 @@
+<?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.
+-->
+
+<!--
+===============================================================
+ PLEASE READ
+===============================================================
+
+The Material themes must not be modified in order to pass CTS.
+Many related themes and styles depend on other values defined in this file.
+If you would like to provide custom themes and styles for your device,
+please see styles_device_defaults.xml.
+
+===============================================================
+ PLEASE READ
+===============================================================
+ -->
+<resources>
+ <!-- Default theme for material style input methods, which is used by the
+ {@link android.inputmethodservice.InputMethodService} class.
+ this inherits from Theme.Panel, but sets up IME appropriate animations
+ and a few custom attributes. -->
+ <style name="Theme.Material.InputMethod" parent="Theme.Material.Panel">
+ <item name="windowAnimationStyle">@style/Animation.InputMethod</item>
+ <item name="imeFullscreenBackground">?colorBackground</item>
+ <item name="imeExtractEnterAnimation">@anim/input_method_extract_enter</item>
+ </style>
+
+ <!-- Override behaviour to set the theme colours for dialogs, keep them the same. -->
+ <style name="ThemeOverlay.Material.Dialog" parent="ThemeOverlay.Material.BaseDialog">
+ <item name="windowIsFloating">false</item>
+ </style>
+
+ <!-- Force the background and floating colours to be the default colours. -->
+ <style name="Theme.Material.Dialog" parent="Theme.Material.BaseDialog">
+ <item name="colorBackground">@color/background_material_dark</item>
+ <item name="colorBackgroundFloating">@color/background_floating_material_dark</item>
+ <item name="colorBackgroundCacheHint">@color/background_cache_hint_selector_material_dark</item>
+ <item name="windowIsFloating">false</item>
+ </style>
+
+ <!-- Force the background and floating colours to be the default colours. -->
+ <style name="Theme.Material.Light.Dialog" parent="Theme.Material.Light.BaseDialog">
+ <item name="colorBackground">@color/background_material_light</item>
+ <item name="colorBackgroundFloating">@color/background_floating_material_light</item>
+ <item name="colorBackgroundCacheHint">@color/background_cache_hint_selector_material_light</item>
+ <item name="windowIsFloating">false</item>
+ </style>
+</resources>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index b61d6cf..13e1d00 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1832,6 +1832,10 @@
<enum name="KEYCODE_CUT" value="277" />
<enum name="KEYCODE_COPY" value="278" />
<enum name="KEYCODE_PASTE" value="279" />
+ <enum name="KEYCODE_FP_NAV_UP" value="280" />
+ <enum name="KEYCODE_FP_NAV_DOWN" value="281" />
+ <enum name="KEYCODE_FP_NAV_LEFT" value="282" />
+ <enum name="KEYCODE_FP_NAV_RIGHT" value="283" />
</attr>
<!-- ***************************************************************** -->
@@ -2043,6 +2047,13 @@
<attr name="showTitle" format="boolean" />
<!-- @hide Whether fullDark, etc. should use default values if null. -->
<attr name="needsDefaultBackgrounds" format="boolean" />
+ <!-- @hide Workaround until we replace AlertController with custom layout. -->
+ <attr name="controllerType">
+ <!-- The default controller. -->
+ <enum name="normal" value="0" />
+ <!-- Controller for micro specific layout. -->
+ <enum name="micro" value="1" />
+ </attr>
</declare-styleable>
<!-- @hide -->
@@ -7324,14 +7335,24 @@
<declare-styleable name="Wallpaper">
<attr name="settingsActivity" />
- <!-- Reference to a the wallpaper's thumbnail bitmap. -->
+ <!-- Reference to the wallpaper's thumbnail bitmap. -->
<attr name="thumbnail" format="reference" />
- <!-- Name of the author of this component, e.g. Google. -->
+ <!-- Name of the author and/or source/collection of this component, e.g. Art Collection, Picasso. -->
<attr name="author" format="reference" />
<!-- Short description of the component's purpose or behavior. -->
<attr name="description" />
+
+ <!-- Uri that specifies a link for further context of this wallpaper, e.g. http://www.picasso.org. -->
+ <attr name="contextUri" format="reference" />
+
+ <!-- Title of the uri that specifies a link for further context of this wallpaper, e.g. Explore collection. -->
+ <attr name="contextDescription" format="reference" />
+
+ <!-- Whether to show any metadata when previewing the wallpaper. -->
+ <attr name="showMetadataInPreview" format="boolean" />
+
</declare-styleable>
<!-- Use <code>dream</code> as the root tag of the XML resource that
@@ -8226,4 +8247,17 @@
color. -->
<attr name="colorBackground" />
</declare-styleable>
+
+ <declare-styleable name="Shortcut">
+ <attr name="shortcutId" format="string" />
+ <attr name="enabled" />
+ <attr name="icon" />
+ <attr name="shortcutShortLabel" format="reference" />
+ <attr name="shortcutLongLabel" format="reference" />
+ <attr name="shortcutDisabledMessage" format="reference" />
+ </declare-styleable>
+
+ <declare-styleable name="ShortcutCategories">
+ <attr name="name" />
+ </declare-styleable>
</resources>
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index d69d73d..0872ef9 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -62,6 +62,20 @@
a reference to a Drawable resource containing the image definition. -->
<attr name="icon" format="reference" />
+ <!-- A Drawable resource providing a graphical representation of its
+ associated item. Use with the
+ application tag (to supply a default round icon for all application
+ components), or with the activity, receiver, service, or instrumentation
+ tag (to supply a specific round icon for that component). It may also be
+ used with the intent-filter tag to supply a round icon to show to the
+ user when an activity is being selected based on a particular Intent.
+
+ <p>The given round icon will be used to display to the user a graphical
+ representation of its associated component; for example, as the round icon
+ for main activity that is displayed in the launcher. This must be
+ a reference to a Drawable resource containing the image definition. -->
+ <attr name="roundIcon" format="reference" />
+
<!-- A Drawable resource providing an extended graphical banner for its
associated item. Use with the application tag (to supply a default
banner for all application activities), or with the activity, tag to
@@ -1236,6 +1250,7 @@
<attr name="theme" />
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="description" />
@@ -1335,6 +1350,7 @@
<attr name="name" />
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="permissionGroup" />
@@ -1362,6 +1378,7 @@
<attr name="name" />
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="description" />
@@ -1395,6 +1412,7 @@
<attr name="name" />
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
</declare-styleable>
@@ -1676,6 +1694,7 @@
<attr name="label" />
<attr name="description" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="process" />
@@ -1759,6 +1778,7 @@
<attr name="label" />
<attr name="description" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="permission" />
@@ -1807,6 +1827,7 @@
<attr name="label" />
<attr name="description" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="permission" />
@@ -1843,6 +1864,7 @@
<attr name="label" />
<attr name="description" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="launchMode" />
@@ -1926,6 +1948,7 @@
<attr name="label" />
<attr name="description" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="permission" />
@@ -1998,6 +2021,7 @@
parent="AndroidManifestActivity AndroidManifestReceiver AndroidManifestService">
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="priority" />
@@ -2128,6 +2152,7 @@
<attr name="targetPackage" />
<attr name="label" />
<attr name="icon" />
+ <attr name="roundIcon" />
<attr name="banner" />
<attr name="logo" />
<attr name="handleProfiling" />
diff --git a/core/res/res/values/colors_device_defaults.xml b/core/res/res/values/colors_device_defaults.xml
new file mode 100644
index 0000000..e830b64
--- /dev/null
+++ b/core/res/res/values/colors_device_defaults.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.
+-->
+
+<!-- Colors specific to DeviceDefault themes. These are mostly pass-throughs to enable
+ overlaying new theme colors. -->
+<resources>
+ <color name="primary_device_default_dark">@color/primary_material_dark</color>
+ <color name="primary_device_default_light">@color/primary_material_light</color>
+ <color name="primary_device_default_settings">@color/primary_material_settings</color>
+ <color name="primary_dark_device_default_dark">@color/primary_dark_material_dark</color>
+ <color name="primary_dark_device_default_light">@color/primary_dark_material_light</color>
+ <color name="primary_dark_device_default_settings">@color/primary_dark_material_settings</color>
+
+ <color name="secondary_device_default_settings">@color/secondary_material_settings</color>
+
+ <color name="accent_device_default_light">@color/accent_material_light</color>
+ <color name="accent_device_default_dark">@color/accent_material_dark</color>
+</resources>
\ No newline at end of file
diff --git a/core/res/res/values/colors_material.xml b/core/res/res/values/colors_material.xml
index c8ca116..a18abdf 100644
--- a/core/res/res/values/colors_material.xml
+++ b/core/res/res/values/colors_material.xml
@@ -26,9 +26,13 @@
<color name="primary_material_dark">@color/material_grey_900</color>
<color name="primary_material_light">@color/material_grey_100</color>
+ <color name="primary_material_settings">@color/material_blue_grey_900</color>
<color name="primary_dark_material_dark">@color/black</color>
<color name="primary_dark_material_light">@color/material_grey_600</color>
<color name="primary_dark_material_light_light_status_bar">@color/material_grey_300</color>
+ <color name="primary_dark_material_settings">@color/material_blue_grey_950</color>
+
+ <color name="secondary_material_settings">@color/material_blue_grey_800</color>
<color name="accent_material_light">@color/material_deep_teal_500</color>
<color name="accent_material_dark">@color/material_deep_teal_200</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 87cb2e8..ca079c4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2464,6 +2464,10 @@
<!-- True if the device supports Sustained Performance Mode-->
<bool name="config_sustainedPerformanceModeSupported">false</bool>
+ <!-- File used to enable the double touch gesture.
+ TODO: move to input HAL once ready. -->
+ <string name="config_doubleTouchGestureEnableFile"></string>
+
<!-- Controls how we deal with externally connected physical keyboards.
0 - When using this device, it is not clear for users to recognize when the physical
keyboard is (should be) connected and when it is (should be) disconnected. Most of
@@ -2489,4 +2493,13 @@
<string-array translatable="false" name="config_defaultPinnerServiceFiles">
</string-array>
+ <!-- Component that is the default launcher when demo mode is enabled. -->
+ <string name="config_demoModeLauncherComponent"></string>
+
+ <!-- Flag indicating whether round icons should be parsed from the application manifest. -->
+ <bool name="config_useRoundIcon">false</bool>
+
+ <!-- True if the device supports system navigation keys. -->
+ <bool name="config_supportSystemNavigationKeys">false</bool>
+
</resources>
diff --git a/core/res/res/values/config_material.xml b/core/res/res/values/config_material.xml
new file mode 100644
index 0000000..a37be83
--- /dev/null
+++ b/core/res/res/values/config_material.xml
@@ -0,0 +1,40 @@
+<?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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds, only for Material theme. Do not translate.
+
+ NOTE: The naming convention is "config_camelCaseValue". -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- True if the device supports action bars. -->
+ <bool name="config_windowActionBarSupported">true</bool>
+
+ <!-- True if the device should have titles by default. -->
+ <bool name="config_windowNoTitleDefault">false</bool>
+
+ <!-- The alert controller to use for alert dialogs. -->
+ <integer name="config_alertDialogController">0</integer>
+
+ <!-- True if windowOverscan should be on by default. -->
+ <bool name="config_windowOverscanByDefault">false</bool>
+
+ <!-- Max number of lines for the dialog title. -->
+ <integer name="config_dialogWindowTitleMaxLines">1</integer>
+
+ <!-- True if preference fragment should clip to padding. -->
+ <bool name="config_preferenceFragmentClipToPadding">true</bool>
+</resources>
diff --git a/core/res/res/values/dimens_material.xml b/core/res/res/values/dimens_material.xml
index 00e48a0..f96cef9 100644
--- a/core/res/res/values/dimens_material.xml
+++ b/core/res/res/values/dimens_material.xml
@@ -24,6 +24,8 @@
<!-- Preference fragment padding, sides -->
<dimen name="preference_fragment_padding_side_material">0dp</dimen>
+ <!-- Preference fragment padding, vertical -->
+ <dimen name="preference_fragment_padding_vertical_material">0dp</dimen>
<!-- Preference breadcrumbs padding, start padding -->
<dimen name="preference_breadcrumbs_padding_start_material">12dp</dimen>
@@ -53,6 +55,8 @@
<!-- Default padding for list items. This should match the action bar
content inset so that ListActivity items line up correctly. -->
<dimen name="list_item_padding_horizontal_material">@dimen/action_bar_content_inset_material</dimen>
+ <dimen name="list_item_padding_start_material">@dimen/action_bar_content_inset_material</dimen>
+ <dimen name="list_item_padding_end_material">@dimen/action_bar_content_inset_material</dimen>
<!-- Padding to add to the start of the overflow action button. -->
<dimen name="action_bar_overflow_padding_start_material">6dp</dimen>
@@ -84,6 +88,8 @@
<dimen name="text_size_medium_material">18sp</dimen>
<dimen name="text_size_small_material">14sp</dimen>
+ <item name="text_line_spacing_multiplier_material" format="float" type="dimen">1.0</item>
+
<dimen name="text_edit_floating_toolbar_elevation">2dp</dimen>
<dimen name="text_edit_floating_toolbar_margin">20dp</dimen>
diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml
index 7f8acd3..5c165e6 100644
--- a/core/res/res/values/ids.xml
+++ b/core/res/res/values/ids.xml
@@ -122,9 +122,11 @@
<!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_SET_PROGRESS}. -->
<item type="id" name="accessibilityActionSetProgress" />
-
+
<!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_CONTEXT_CLICK}. -->
<item type="id" name="accessibilityActionContextClick" />
<item type="id" name="remote_input_tag" />
+
+ <item type="id" name="cross_task_transition" />
</resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 6797799..c187d2c 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2728,4 +2728,18 @@
<public type="id" name="icon_frame" id="0x0102003e" />
<public type="id" name="list_container" id="0x0102003f" />
<public type="id" name="switch_widget" id="0x01020040" />
+ <!-- ===============================================================
+ Resources added in version N MR1 of the platform
+ =============================================================== -->
+ <eat-comment />
+ <public type="attr" name="shortcutId" />
+ <public type="attr" name="shortcutShortLabel" />
+ <public type="attr" name="shortcutLongLabel" />
+ <public type="attr" name="shortcutDisabledMessage" />
+ <public type="attr" name="roundIcon" />
+
+ <public type="attr" name="contextUri" />
+ <public type="attr" name="contextDescription" />
+ <public type="attr" name="showMetadataInPreview" />
+
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b55a9b22..c036c36 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4371,6 +4371,17 @@
<!-- 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 to start a new demo session when device is in retail mode [CHAR LIMIT=NONE] -->
+ <string name="reset_retail_demo_mode_title">Restart Session</string>
+ <!-- Text of notification to start a new demo session when device is in retail mode [CHAR LIMIT=NONE] -->
+ <string name="reset_retail_demo_mode_text">Tap to start a new demo session</string>
+ <!-- Text of dialog shown when starting a demo user for the first time [CHAR LIMIT=40] -->
+ <string name="demo_starting_message">Starting demo</string>
+ <!-- Text of dialog shown when starting a new demo user in retail demo mode [CHAR LIMIT=40] -->
+ <string name="demo_restarting_message">Restarting session</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 without restrictions</string>
<!-- Description of notification shown when device has been forced to safe mode after a security compromise. -->
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 790dcfa..273086d 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1122,6 +1122,8 @@
<style name="PreferenceFragmentList">
<item name="paddingStart">@dimen/preference_fragment_padding_side</item>
<item name="paddingEnd">@dimen/preference_fragment_padding_side</item>
+ <item name="paddingTop">0dp</item>
+ <item name="paddingBottom">@dimen/preference_fragment_padding_bottom</item>
</style>
<!-- Other Misc Styles -->
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index bb07834..6e0ad36 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -40,7 +40,10 @@
<item name="layout">@layout/preference_list_fragment_material</item>
<item name="paddingStart">@dimen/preference_fragment_padding_side_material</item>
<item name="paddingEnd">@dimen/preference_fragment_padding_side_material</item>
+ <item name="paddingTop">@dimen/preference_fragment_padding_vertical_material</item>
+ <item name="paddingBottom">@dimen/preference_fragment_padding_vertical_material</item>
<item name="divider">?attr/listDivider</item>
+ <item name="clipToPadding">@bool/config_preferenceFragmentClipToPadding</item>
</style>
<style name="PreferenceActivity.Material">
@@ -134,6 +137,8 @@
<style name="PreferenceFragmentList.Material">
<item name="paddingStart">@dimen/preference_fragment_padding_side_material</item>
<item name="paddingEnd">@dimen/preference_fragment_padding_side_material</item>
+ <item name="paddingTop">@dimen/preference_fragment_padding_vertical_material</item>
+ <item name="paddingBottom">@dimen/preference_fragment_padding_vertical_material</item>
</style>
<!-- Begin Material theme styles -->
@@ -147,6 +152,7 @@
<item name="textColorLink">?attr/textColorLink</item>
<item name="textSize">@dimen/text_size_body_1_material</item>
<item name="fontFamily">@string/font_family_body_1_material</item>
+ <item name="lineSpacingMultiplier">@dimen/text_line_spacing_multiplier_material</item>
</style>
<style name="TextAppearance.Material.Display4">
@@ -1190,6 +1196,7 @@
<item name="listItemLayout">@layout/select_dialog_item_material</item>
<item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
<item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
+ <item name="controllerType">@integer/config_alertDialogController</item>
</style>
<style name="AlertDialog.Material.Light" />
@@ -1224,7 +1231,7 @@
<style name="DialogWindowTitleBackground.Material.Light" />
<style name="DialogWindowTitle.Material">
- <item name="maxLines">1</item>
+ <item name="maxLines">@integer/config_dialogWindowTitleMaxLines</item>
<item name="scrollHorizontally">true</item>
<item name="textAppearance">@style/TextAppearance.Material.DialogWindowTitle</item>
</style>
diff --git a/core/res/res/values/styles_micro.xml b/core/res/res/values/styles_micro.xml
deleted file mode 100644
index 341a0a4..0000000
--- a/core/res/res/values/styles_micro.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2014 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.
--->
-<resources>
- <style name="Animation.Micro"/>
-
- <style name="Animation.Micro.Activity" parent="Animation.Material.Activity">
- <item name="activityOpenEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="activityOpenRemoteViewsEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="activityOpenExitAnimation">@anim/slide_in_exit_micro</item>
- <item name="activityCloseEnterAnimation">@null</item>
- <item name="activityCloseExitAnimation">@anim/slide_out_micro</item>
- <item name="taskOpenEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="taskOpenExitAnimation">@anim/slide_in_exit_micro</item>
- <item name="taskCloseEnterAnimation">@null</item>
- <item name="taskCloseExitAnimation">@anim/slide_out_micro</item>
- <item name="taskToFrontEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="taskToFrontExitAnimation">@anim/slide_in_exit_micro</item>
- <item name="taskToBackEnterAnimation">@null</item>
- <item name="taskToBackExitAnimation">@anim/slide_out_micro</item>
- <item name="wallpaperOpenEnterAnimation">@null</item>
- <item name="wallpaperOpenExitAnimation">@anim/slide_out_micro</item>
- <item name="wallpaperCloseEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="wallpaperCloseExitAnimation">@anim/slide_in_exit_micro</item>
- <item name="wallpaperIntraOpenEnterAnimation">@null</item>
- <item name="wallpaperIntraOpenExitAnimation">@anim/slide_out_micro</item>
- <item name="wallpaperIntraCloseEnterAnimation">@anim/slide_in_enter_micro</item>
- <item name="wallpaperIntraCloseExitAnimation">@anim/slide_in_exit_micro</item>
- </style>
-
- <style name="AlertDialog.Micro" parent="AlertDialog.Material.Light">
- <item name="fullDark">@null</item>
- <item name="topDark">@null</item>
- <item name="centerDark">@null</item>
- <item name="bottomDark">@null</item>
- <item name="fullBright">@null</item>
- <item name="topBright">@null</item>
- <item name="centerBright">@null</item>
- <item name="bottomBright">@null</item>
- <item name="bottomMedium">@null</item>
- <item name="centerMedium">@null</item>
- <item name="layout">@layout/alert_dialog_micro</item>
- </style>
-
- <style name="DialogWindowTitle.Micro">
- <item name="maxLines">1</item>
- <item name="scrollHorizontally">true</item>
- <item name="textAppearance">@style/TextAppearance.Micro.DialogWindowTitle</item>
- </style>
-
- <style name="TextAppearance.Micro" parent="TextAppearance.Material">
- <item name="textSize">20sp</item>
- <item name="fontFamily">sans-serif-condensed-light</item>
- <item name="textColor">@color/micro_text_light</item>
- </style>
-
- <style name="TextAppearance.Micro.DialogWindowTitle" parent="TextAppearance.Material.DialogWindowTitle">
- <item name="textSize">20sp</item>
- <item name="fontFamily">sans-serif-condensed-light</item>
- <item name="textColor">@color/micro_text_light</item>
- </style>
-
- <style name="Widget.Micro" parent="Widget.Material" />
-
- <style name="Widget.Micro.TextView">
- <item name="fontFamily">sans-serif-condensed</item>
- </style>
-
- <style name="Widget.Micro.NumberPicker">
- <item name="internalLayout">@layout/number_picker_with_selector_wheel_micro</item>
- <item name="solidColor">@color/transparent</item>
- <item name="selectionDivider">@drawable/numberpicker_selection_divider</item>
- <item name="selectionDividerHeight">0dip</item>
- <item name="selectionDividersDistance">104dip</item>
- <item name="internalMinWidth">64dip</item>
- <item name="internalMaxHeight">180dip</item>
- <item name="virtualButtonPressedDrawable">?attr/selectableItemBackground</item>
- <item name="descendantFocusability">blocksDescendants</item>
- </style>
-
-</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d154b03..385c61a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1103,6 +1103,10 @@
<java-symbol type="string" name="lockscreen_transport_pause_description" />
<java-symbol type="string" name="config_ethernet_tcp_buffers" />
<java-symbol type="string" name="config_wifi_tcp_buffers" />
+ <java-symbol type="string" name="config_demoModeLauncherComponent" />
+ <java-symbol type="string" name="demo_starting_message" />
+ <java-symbol type="string" name="demo_restarting_message" />
+
<java-symbol type="plurals" name="bugreport_countdown" />
<java-symbol type="plurals" name="duration_hours" />
@@ -1859,7 +1863,6 @@
<java-symbol type="string" name="vpn_lockdown_config" />
<java-symbol type="string" name="wallpaper_binding_label" />
<java-symbol type="style" name="Theme.Dialog.AppError" />
- <java-symbol type="style" name="Theme.Micro.Dialog.Alert" />
<java-symbol type="style" name="Theme.Leanback.Dialog.Alert" />
<java-symbol type="style" name="Theme.Toast" />
<java-symbol type="xml" name="storage_list" />
@@ -1887,6 +1890,8 @@
<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="string" name="reset_retail_demo_mode_title" />
+ <java-symbol type="string" name="reset_retail_demo_mode_text" />
<java-symbol type="layout" name="resolver_list" />
<java-symbol type="id" name="resolver_list" />
@@ -2183,6 +2188,7 @@
<java-symbol type="style" name="TextAppearance.Material.TimePicker.TimeLabel" />
<java-symbol type="attr" name="seekBarPreferenceStyle" />
<java-symbol type="style" name="Theme.DeviceDefault.Resolver" />
+ <java-symbol type="style" name="Theme.DeviceDefault.System" />
<java-symbol type="attr" name="preferenceActivityStyle" />
<java-symbol type="attr" name="preferenceFragmentStyle" />
<java-symbol type="bool" name="skipHoldBeforeMerge" />
@@ -2608,8 +2614,18 @@
<!-- Pinner Service -->
<java-symbol type="array" name="config_defaultPinnerServiceFiles" />
+ <java-symbol type="string" name="config_doubleTouchGestureEnableFile" />
+
<java-symbol type="string" name="suspended_widget_accessibility" />
+ <!-- Used internally for assistant to launch activity transitions -->
+ <java-symbol type="id" name="cross_task_transition" />
+
+ <java-symbol type="bool" name="config_useRoundIcon" />
+
+ <!-- For System navigation keys -->
+ <java-symbol type="bool" name="config_supportSystemNavigationKeys" />
+
<java-symbol type="layout" name="unsupported_display_size_dialog_content" />
<java-symbol type="string" name="unsupported_display_size_message" />
</resources>
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 11bb106..e5b6c78 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -199,25 +199,50 @@
<item name="mediaRouteButtonStyle">@style/Widget.DeviceDefault.MediaRouteButton</item>
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+
</style>
<!-- Variant of {@link #Theme_DeviceDefault} with no action bar -->
- <style name="Theme.DeviceDefault.NoActionBar" parent="Theme.Material.NoActionBar" />
+ <style name="Theme.DeviceDefault.NoActionBar" parent="Theme.Material.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault} with no action bar and no status bar. This theme
sets {@link android.R.attr#windowFullscreen} to true. -->
- <style name="Theme.DeviceDefault.NoActionBar.Fullscreen" parent="Theme.Material.NoActionBar.Fullscreen" />
+ <style name="Theme.DeviceDefault.NoActionBar.Fullscreen" parent="Theme.Material.NoActionBar.Fullscreen">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault} with no action bar and no status bar and
extending in to overscan region. This theme
sets {@link android.R.attr#windowFullscreen} and {@link android.R.attr#windowOverscan}
to true. -->
- <style name="Theme.DeviceDefault.NoActionBar.Overscan" parent="Theme.Material.NoActionBar.Overscan" />
+ <style name="Theme.DeviceDefault.NoActionBar.Overscan" parent="Theme.Material.NoActionBar.Overscan">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault} that has no title bar and translucent
system decor. This theme sets {@link android.R.attr#windowTranslucentStatus} and
{@link android.R.attr#windowTranslucentNavigation} to true. -->
- <style name="Theme.DeviceDefault.NoActionBar.TranslucentDecor" parent="Theme.Material.NoActionBar.TranslucentDecor" />
+ <style name="Theme.DeviceDefault.NoActionBar.TranslucentDecor" parent="Theme.Material.NoActionBar.TranslucentDecor">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for dialog windows and activities. This changes the window to be
floating (not fill the entire screen), and puts a frame around its contents. You can set this
@@ -231,18 +256,38 @@
<item name="textAppearance">@style/TextAppearance.DeviceDefault</item>
<item name="textAppearanceInverse">@style/TextAppearance.DeviceDefault.Inverse</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
</style>
<!-- Variant of {@link #Theme_DeviceDefault_Dialog} that has a nice minimum width for a
regular dialog. -->
- <style name="Theme.DeviceDefault.Dialog.MinWidth" parent="Theme.Material.Dialog.MinWidth" />
+ <style name="Theme.DeviceDefault.Dialog.MinWidth" parent="Theme.Material.Dialog.MinWidth">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Dialog} without an action bar -->
- <style name="Theme.DeviceDefault.Dialog.NoActionBar" parent="Theme.Material.Dialog.NoActionBar" />
+ <style name="Theme.DeviceDefault.Dialog.NoActionBar" parent="Theme.Material.Dialog.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Dialog_NoActionBar} that has a nice minimum width
for a regular dialog. -->
- <style name="Theme.DeviceDefault.Dialog.NoActionBar.MinWidth" parent="Theme.Material.Dialog.NoActionBar.MinWidth" />
+ <style name="Theme.DeviceDefault.Dialog.NoActionBar.MinWidth" parent="Theme.Material.Dialog.NoActionBar.MinWidth">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of Theme.DeviceDefault.Dialog that has a fixed size. -->
<style name="Theme.DeviceDefault.Dialog.FixedSize">
@@ -262,44 +307,99 @@
<!-- DeviceDefault theme for a window that will be displayed either full-screen on smaller
screens (small, normal) or as a dialog on larger screens (large, xlarge). -->
- <style name="Theme.DeviceDefault.DialogWhenLarge" parent="Theme.Material.DialogWhenLarge" />
+ <style name="Theme.DeviceDefault.DialogWhenLarge" parent="Theme.Material.DialogWhenLarge">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for a window without an action bar that will be displayed either
full-screen on smaller screens (small, normal) or as a dialog on larger screens (large,
xlarge). -->
- <style name="Theme.DeviceDefault.DialogWhenLarge.NoActionBar" parent="Theme.Material.DialogWhenLarge.NoActionBar" />
+ <style name="Theme.DeviceDefault.DialogWhenLarge.NoActionBar" parent="Theme.Material.DialogWhenLarge.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for a presentation window on a secondary display. -->
- <style name="Theme.DeviceDefault.Dialog.Presentation" parent="Theme.Material.Dialog.Presentation" />
+ <style name="Theme.DeviceDefault.Dialog.Presentation" parent="Theme.Material.Dialog.Presentation">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for panel windows. This removes all extraneous window
decorations, so you basically have an empty rectangle in which to place your content. It makes
the window floating, with a transparent background, and turns off dimming behind the window. -->
- <style name="Theme.DeviceDefault.Panel" parent="Theme.Material.Panel" />
+ <style name="Theme.DeviceDefault.Panel" parent="Theme.Material.Panel">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
behind them. -->
- <style name="Theme.DeviceDefault.Wallpaper" parent="Theme.Material.Wallpaper" />
+ <style name="Theme.DeviceDefault.Wallpaper" parent="Theme.Material.Wallpaper">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
behind them and without an action bar. -->
- <style name="Theme.DeviceDefault.Wallpaper.NoTitleBar" parent="Theme.Material.Wallpaper.NoTitleBar" />
+ <style name="Theme.DeviceDefault.Wallpaper.NoTitleBar" parent="Theme.Material.Wallpaper.NoTitleBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- DeviceDefault style for input methods, which is used by the
{@link android.inputmethodservice.InputMethodService} class.-->
- <style name="Theme.DeviceDefault.InputMethod" parent="Theme.Material.InputMethod" />
+ <style name="Theme.DeviceDefault.InputMethod" parent="Theme.Material.InputMethod">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- DeviceDefault style for input methods, which is used by the
{@link android.service.voice.VoiceInteractionSession} class.-->
- <style name="Theme.DeviceDefault.VoiceInteractionSession" parent="Theme.Material.VoiceInteractionSession" >
-
+ <style name="Theme.DeviceDefault.VoiceInteractionSession" parent="Theme.Material.VoiceInteractionSession">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
+
<style name="Theme.DeviceDefault.Dialog.Alert" parent="Theme.Material.Dialog.Alert">
<item name="windowTitleStyle">@style/DialogWindowTitle.DeviceDefault</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
</style>
- <style name="Theme.DeviceDefault.SearchBar" parent="Theme.Material.SearchBar" />
- <style name="Theme.DeviceDefault.Dialog.NoFrame" parent="Theme.Material.Dialog.NoFrame" />
+ <style name="Theme.DeviceDefault.SearchBar" parent="Theme.Material.SearchBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Dialog.NoFrame" parent="Theme.Material.Dialog.NoFrame">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_dark</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault} with a light-colored style -->
<style name="Theme.DeviceDefault.Light" parent="Theme.Material.Light" >
@@ -447,34 +547,63 @@
<item name="mediaRouteButtonStyle">@style/Widget.DeviceDefault.Light.MediaRouteButton</item>
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
<!-- Variant of the DeviceDefault (light) theme that has a solid (opaque) action bar with an
inverse color profile. -->
- <style name="Theme.DeviceDefault.Light.DarkActionBar" parent="Theme.Material.Light.DarkActionBar" />
+ <style name="Theme.DeviceDefault.Light.DarkActionBar" parent="Theme.Material.Light.DarkActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_dark</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar -->
- <style name="Theme.DeviceDefault.Light.NoActionBar" parent="Theme.Material.Light.NoActionBar" />
+ <style name="Theme.DeviceDefault.Light.NoActionBar" parent="Theme.Material.Light.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar and no status bar.
This theme sets {@link android.R.attr#windowFullscreen} to true. -->
- <style name="Theme.DeviceDefault.Light.NoActionBar.Fullscreen" parent="Theme.Material.Light.NoActionBar.Fullscreen" />
+ <style name="Theme.DeviceDefault.Light.NoActionBar.Fullscreen" parent="Theme.Material.Light.NoActionBar.Fullscreen">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar and no status bar
and extending in to overscan region. This theme
sets {@link android.R.attr#windowFullscreen} and {@link android.R.attr#windowOverscan}
to true. -->
- <style name="Theme.DeviceDefault.Light.NoActionBar.Overscan" parent="Theme.Material.Light.NoActionBar.Overscan" />
+ <style name="Theme.DeviceDefault.Light.NoActionBar.Overscan" parent="Theme.Material.Light.NoActionBar.Overscan">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light} that has no title bar and translucent
system decor. This theme sets {@link android.R.attr#windowTranslucentStatus} and
{@link android.R.attr#windowTranslucentNavigation} to true. -->
- <style name="Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor" parent="Theme.Material.Light.NoActionBar.TranslucentDecor" />
+ <style name="Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor" parent="Theme.Material.Light.NoActionBar.TranslucentDecor">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- DeviceDefault light theme for dialog windows and activities. This changes the window to be
floating (not fill the entire screen), and puts a frame around its contents. You can set this
theme on an activity if you would like to make an activity that looks like a Dialog.-->
- <style name="Theme.DeviceDefault.Light.Dialog" parent="Theme.Material.Light.Dialog" >
+ <style name="Theme.DeviceDefault.Light.Dialog" parent="Theme.Material.Light.Dialog">
<item name="windowTitleStyle">@style/DialogWindowTitle.DeviceDefault.Light</item>
<item name="windowAnimationStyle">@style/Animation.DeviceDefault.Dialog</item>
@@ -483,18 +612,38 @@
<item name="textAppearance">@style/TextAppearance.DeviceDefault</item>
<item name="textAppearanceInverse">@style/TextAppearance.DeviceDefault.Inverse</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
<!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog} that has a nice minimum width for a
regular dialog. -->
- <style name="Theme.DeviceDefault.Light.Dialog.MinWidth" parent="Theme.Material.Light.Dialog.MinWidth" />
+ <style name="Theme.DeviceDefault.Light.Dialog.MinWidth" parent="Theme.Material.Light.Dialog.MinWidth">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog} without an action bar -->
- <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar" parent="Theme.Material.Light.Dialog.NoActionBar" />
+ <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar" parent="Theme.Material.Light.Dialog.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog_NoActionBar} that has a nice minimum
width for a regular dialog. -->
- <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth" parent="Theme.Material.Light.Dialog.NoActionBar.MinWidth" />
+ <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth" parent="Theme.Material.Light.Dialog.NoActionBar.MinWidth">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Variant of Theme.DeviceDefault.Dialog that has a fixed size. -->
<style name="Theme.DeviceDefault.Light.Dialog.FixedSize">
@@ -502,6 +651,11 @@
<item name="windowFixedWidthMinor">@dimen/dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/dialog_fixed_height_major</item>
<item name="windowFixedHeightMinor">@dimen/dialog_fixed_height_minor</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
<!-- Variant of Theme.DeviceDefault.Dialog.NoActionBar that has a fixed size. -->
@@ -510,33 +664,74 @@
<item name="windowFixedWidthMinor">@dimen/dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/dialog_fixed_height_major</item>
<item name="windowFixedHeightMinor">@dimen/dialog_fixed_height_minor</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
<!-- DeviceDefault light theme for a window that will be displayed either full-screen on smaller
screens (small, normal) or as a dialog on larger screens (large, xlarge). -->
- <style name="Theme.DeviceDefault.Light.DialogWhenLarge" parent="Theme.Material.Light.DialogWhenLarge" />
+ <style name="Theme.DeviceDefault.Light.DialogWhenLarge" parent="Theme.Material.Light.DialogWhenLarge">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- DeviceDefault light theme for a window without an action bar that will be displayed either
full-screen on smaller screens (small, normal) or as a dialog on larger screens (large,
xlarge). -->
- <style name="Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar" parent="Theme.Material.Light.DialogWhenLarge.NoActionBar" />
+ <style name="Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar" parent="Theme.Material.Light.DialogWhenLarge.NoActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- DeviceDefault light theme for a presentation window on a secondary display. -->
- <style name="Theme.DeviceDefault.Light.Dialog.Presentation" parent="Theme.Material.Light.Dialog.Presentation" />
+ <style name="Theme.DeviceDefault.Light.Dialog.Presentation" parent="Theme.Material.Light.Dialog.Presentation">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- DeviceDefault light theme for panel windows. This removes all extraneous window
decorations, so you basically have an empty rectangle in which to place your content. It makes
the window floating, with a transparent background, and turns off dimming behind the window. -->
- <style name="Theme.DeviceDefault.Light.Panel" parent="Theme.Material.Light.Panel" />
+ <style name="Theme.DeviceDefault.Light.Panel" parent="Theme.Material.Light.Panel">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="Theme.Material.Light.Dialog.Alert">
<item name="windowTitleStyle">@style/DialogWindowTitle.DeviceDefault.Light</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
- <style name="Theme.DeviceDefault.Light.SearchBar" parent="Theme.Material.Light.SearchBar" />
+ <style name="Theme.DeviceDefault.Light.SearchBar" parent="Theme.Material.Light.SearchBar">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
+
<!-- DeviceDefault theme for a window that should look like the Settings app. -->
- <style name="Theme.DeviceDefault.Settings" parent="Theme.Material.Settings" />
+ <style name="Theme.DeviceDefault.Settings" parent="Theme.Material.Settings">
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_settings</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ </style>
<!-- Theme used for the intent picker activity. -->
<style name="Theme.DeviceDefault.Resolver" parent="Theme.Material.Light">
@@ -549,6 +744,13 @@
<item name="colorControlActivated">?attr/colorControlHighlight</item>
<item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
<item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_dark_device_default_light</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
</style>
+ <!-- DeviceDefault theme for the default system theme. -->
+ <style name="Theme.DeviceDefault.System" parent="Theme.DeviceDefault.Light.DarkActionBar" />
</resources>
diff --git a/core/res/res/values/themes_material.xml b/core/res/res/values/themes_material.xml
index 2ea5c5e..d437032 100644
--- a/core/res/res/values/themes_material.xml
+++ b/core/res/res/values/themes_material.xml
@@ -119,8 +119,8 @@
<item name="textAppearanceListItemSecondary">@style/TextAppearance.Material.Body1</item>
<item name="listPreferredItemPaddingLeft">@dimen/list_item_padding_horizontal_material</item>
<item name="listPreferredItemPaddingRight">@dimen/list_item_padding_horizontal_material</item>
- <item name="listPreferredItemPaddingStart">@dimen/list_item_padding_horizontal_material</item>
- <item name="listPreferredItemPaddingEnd">@dimen/list_item_padding_horizontal_material</item>
+ <item name="listPreferredItemPaddingStart">@dimen/list_item_padding_start_material</item>
+ <item name="listPreferredItemPaddingEnd">@dimen/list_item_padding_end_material</item>
<!-- @hide -->
<item name="searchResultListItemHeight">58dip</item>
@@ -153,9 +153,9 @@
<item name="windowBackground">?attr/colorBackground</item>
<item name="windowClipToOutline">true</item>
<item name="windowFrame">@null</item>
- <item name="windowNoTitle">false</item>
+ <item name="windowNoTitle">@bool/config_windowNoTitleDefault</item>
<item name="windowFullscreen">false</item>
- <item name="windowOverscan">false</item>
+ <item name="windowOverscan">@bool/config_windowOverscanByDefault</item>
<item name="windowIsFloating">false</item>
<item name="windowContentOverlay">@null</item>
<item name="windowShowWallpaper">false</item>
@@ -164,7 +164,7 @@
<item name="windowTitleBackgroundStyle">@style/WindowTitleBackground.Material</item>
<item name="windowAnimationStyle">@style/Animation.Material.Activity</item>
<item name="windowSoftInputMode">stateUnspecified|adjustUnspecified</item>
- <item name="windowActionBar">true</item>
+ <item name="windowActionBar">@bool/config_windowActionBarSupported</item>
<item name="windowActionModeOverlay">false</item>
<item name="windowDrawsSystemBarBackgrounds">true</item>
<item name="windowActionBarFullscreenDecorLayout">@layout/screen_toolbar</item>
@@ -480,8 +480,8 @@
<item name="textAppearanceListItemSecondary">@style/TextAppearance.Material.Body1</item>
<item name="listPreferredItemPaddingLeft">@dimen/list_item_padding_horizontal_material</item>
<item name="listPreferredItemPaddingRight">@dimen/list_item_padding_horizontal_material</item>
- <item name="listPreferredItemPaddingStart">@dimen/list_item_padding_horizontal_material</item>
- <item name="listPreferredItemPaddingEnd">@dimen/list_item_padding_horizontal_material</item>
+ <item name="listPreferredItemPaddingStart">@dimen/list_item_padding_start_material</item>
+ <item name="listPreferredItemPaddingEnd">@dimen/list_item_padding_end_material</item>
<!-- @hide -->
<item name="searchResultListItemHeight">58dip</item>
@@ -514,9 +514,9 @@
<item name="windowBackground">?attr/colorBackground</item>
<item name="windowClipToOutline">true</item>
<item name="windowFrame">@null</item>
- <item name="windowNoTitle">false</item>
+ <item name="windowNoTitle">@bool/config_windowNoTitleDefault</item>
<item name="windowFullscreen">false</item>
- <item name="windowOverscan">false</item>
+ <item name="windowOverscan">@bool/config_windowOverscanByDefault</item>
<item name="windowIsFloating">false</item>
<item name="windowContentOverlay">@null</item>
<item name="windowShowWallpaper">false</item>
@@ -525,7 +525,7 @@
<item name="windowTitleBackgroundStyle">@style/WindowTitleBackground.Material</item>
<item name="windowAnimationStyle">@style/Animation.Material.Activity</item>
<item name="windowSoftInputMode">stateUnspecified|adjustUnspecified</item>
- <item name="windowActionBar">true</item>
+ <item name="windowActionBar">@bool/config_windowActionBarSupported</item>
<item name="windowActionModeOverlay">false</item>
<item name="windowDrawsSystemBarBackgrounds">true</item>
<item name="windowActionBarFullscreenDecorLayout">@layout/screen_toolbar</item>
@@ -864,11 +864,8 @@
<item name="searchViewStyle">@style/Widget.Material.SearchView.ActionBar</item>
</style>
- <!-- Theme overlay that overrides window properties to display as a dialog. -->
- <style name="ThemeOverlay.Material.Dialog">
- <item name="colorBackgroundCacheHint">@null</item>
- <item name="colorBackground">?attr/colorBackgroundFloating</item>
-
+ <!-- Base theme for overlay dialogs, customize the colours in the actual dialog theme. -->
+ <style name="ThemeOverlay.Material.BaseDialog">
<item name="windowFrame">@null</item>
<item name="windowTitleStyle">@style/DialogWindowTitle.Material</item>
<item name="windowTitleBackgroundStyle">@style/DialogWindowTitleBackground.Material</item>
@@ -897,6 +894,12 @@
<item name="windowFixedHeightMinor">@null</item>
</style>
+ <!-- Theme overlay that overrides window properties to display as a dialog. -->
+ <style name="ThemeOverlay.Material.Dialog" parent="ThemeOverlay.Material.BaseDialog">
+ <item name="colorBackgroundCacheHint">@null</item>
+ <item name="colorBackground">?attr/colorBackgroundFloating</item>
+ </style>
+
<!-- Theme overlay that overrides window properties to display as a date picker dialog. -->
<style name="ThemeOverlay.Material.Dialog.DatePicker">
<item name="alertDialogStyle">@style/DatePickerDialog.Material</item>
@@ -1090,10 +1093,10 @@
<item name="colorBackgroundCacheHint">@null</item>
- <item name="listPreferredItemPaddingLeft">24dip</item>
- <item name="listPreferredItemPaddingRight">24dip</item>
- <item name="listPreferredItemPaddingStart">24dip</item>
- <item name="listPreferredItemPaddingEnd">24dip</item>
+ <item name="listPreferredItemPaddingLeft">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingRight">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
<item name="listDivider">@null</item>
@@ -1201,10 +1204,10 @@
<item name="colorBackgroundCacheHint">@null</item>
- <item name="listPreferredItemPaddingLeft">24dip</item>
- <item name="listPreferredItemPaddingRight">24dip</item>
- <item name="listPreferredItemPaddingStart">24dip</item>
- <item name="listPreferredItemPaddingEnd">24dip</item>
+ <item name="listPreferredItemPaddingLeft">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingRight">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
<item name="listDivider">@null</item>
@@ -1300,8 +1303,8 @@
<!-- Default theme for Settings and activities launched from Settings. -->
<style name="Theme.Material.Settings" parent="Theme.Material.Light.DarkActionBar">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
<item name="presentationTheme">@style/Theme.Material.Settings.Dialog.Presentation</item>
<item name="searchDialogTheme">@style/Theme.Material.Settings.SearchBar</item>
@@ -1310,8 +1313,8 @@
<!-- Default theme for Settings and activities launched from Settings. -->
<style name="Theme.Material.Settings.NoActionBar" parent="Theme.Material.Light.NoActionBar">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
<item name="presentationTheme">@style/Theme.Material.Settings.Dialog.Presentation</item>
<item name="searchDialogTheme">@style/Theme.Material.Settings.SearchBar</item>
@@ -1319,41 +1322,41 @@
</style>
<style name="Theme.Material.Settings.BaseDialog" parent="Theme.Material.Light.BaseDialog">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.Dialog" parent="Theme.Material.Settings.BaseDialog" />
<style name="Theme.Material.Settings.Dialog.BaseAlert" parent="Theme.Material.Light.Dialog.BaseAlert">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.Dialog.Alert" parent="Theme.Material.Settings.Dialog.BaseAlert" />
<style name="Theme.Material.Settings.DialogWhenLarge" parent="Theme.Material.Light.DialogWhenLarge.DarkActionBar">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.DialogWhenLarge.NoActionBar" parent="Theme.Material.Light.DialogWhenLarge.NoActionBar">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.Dialog.Presentation" parent="Theme.Material.Light.Dialog.Presentation">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.SearchBar" parent="Theme.Material.Light.SearchBar">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
<style name="Theme.Material.Settings.CompactMenu" parent="Theme.Material.Light.CompactMenu">
- <item name="colorPrimary">@color/material_blue_grey_900</item>
- <item name="colorPrimaryDark">@color/material_blue_grey_950</item>
+ <item name="colorPrimary">@color/primary_material_settings</item>
+ <item name="colorPrimaryDark">@color/primary_dark_material_settings</item>
</style>
</resources>
diff --git a/core/res/res/values/themes_micro.xml b/core/res/res/values/themes_micro.xml
deleted file mode 100644
index 25a6e00..0000000
--- a/core/res/res/values/themes_micro.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2014 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.
--->
-<resources>
- <style name="Theme.MicroBase" parent="Theme.Material.NoActionBar">
- <item name="alertDialogTheme">@style/Theme.Micro.Dialog.Alert</item>
- <item name="alertDialogStyle">@style/AlertDialog.Micro</item>
- <item name="dialogTheme">@style/Theme.Micro.Dialog</item>
- <item name="textViewStyle">@style/Widget.Micro.TextView</item>
- <item name="numberPickerStyle">@style/Widget.Micro.NumberPicker</item>
- <item name="windowAnimationStyle">@style/Animation.Micro.Activity</item>
- <item name="windowBackground">@color/black</item>
- <item name="windowContentOverlay">@null</item>
- <item name="windowIsFloating">false</item>
- <!-- Required to force windowInsets dispatch through application UI. -->
- <item name="windowOverscan">true</item>
- </style>
-
- <style name="Theme.Micro" parent="Theme.MicroBase">
- </style>
-
- <style name="Theme.Micro.LightBase" parent="Theme.Material.Light.NoActionBar">
- <item name="alertDialogTheme">@style/Theme.Micro.Dialog.Alert</item>
- <item name="alertDialogStyle">@style/AlertDialog.Micro</item>
- <item name="dialogTheme">@style/Theme.Micro.Dialog</item>
- <item name="textViewStyle">@style/Widget.Micro.TextView</item>
- <item name="numberPickerStyle">@style/Widget.Micro.NumberPicker</item>
- <item name="windowAnimationStyle">@style/Animation.Micro.Activity</item>
- <item name="windowBackground">@color/white</item>
- <item name="windowContentOverlay">@null</item>
- <item name="windowIsFloating">false</item>
- <!-- Required to force windowInsets dispatch through application UI. -->
- <item name="windowOverscan">true</item>
- </style>
-
- <!-- Indirection needed for overlays to make sure there is a common base parent -->
- <style name="Theme.Micro.Light" parent="Theme.Micro.LightBase">
- </style>
-
- <style name="Theme.Micro.DialogBase" parent="Theme.Material.Light.Dialog">
- <item name="windowTitleStyle">@android:style/DialogWindowTitle.Micro</item>
- <item name="windowIsFloating">false</item>
- <item name="windowFullscreen">true</item>
- <item name="textAppearance">@style/TextAppearance.Micro</item>
- <item name="textAppearanceInverse">@style/TextAppearance.Micro</item>
- <!-- Required to force windowInsets dispatch through application UI. -->
- <item name="windowOverscan">true</item>
- </style>
-
- <!-- Indirection needed for overlays to make sure there is a common base parent -->
- <style name="Theme.Micro.Dialog" parent="Theme.Micro.DialogBase">
- </style>
-
- <style name="Theme.Micro.Dialog.Alert">
- <item name="windowTitleStyle">@style/DialogWindowTitle.Micro</item>
- <item name="alertDialogStyle">@style/AlertDialog.Micro</item>
- <item name="windowIsFloating">false</item>
- <item name="windowBackground">@android:color/transparent</item>
- <item name="windowOverscan">true</item>
- <item name="windowContentOverlay">@null</item>
- <item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
- <item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
- </style>
-
- <style name="Theme.Micro.Dialog.AppError" parent="Theme.Micro.Dialog">
- <item name="windowBackground">@null</item>
- <item name="alertDialogStyle">@style/AlertDialog.Micro</item>
- <item name="windowOverscan">true</item>
- <item name="windowCloseOnTouchOutside">false</item>
- <item name="textSize">20sp</item>
- <item name="fontFamily">sans-serif-condensed-light</item>
- <item name="textColor">@color/micro_text_light</item>
- </style>
-
- <style name="Theme.Micro.Panel" parent="Theme.Material.Panel" />
- <style name="Theme.Micro.Light.Panel" parent="Theme.Material.Light.Panel" />
-
- <!-- Default theme for material style input methods, which is used by the
- {@link android.inputmethodservice.InputMethodService} class.
- This inherits from Theme.Panel, but sets up IME appropriate animations
- and a few custom attributes. -->
- <style name="Theme.Micro.InputMethod" parent="Theme.Micro.Panel">
- <item name="windowAnimationStyle">@style/Animation.InputMethod</item>
- <item name="imeFullscreenBackground">#1e282c</item>
- <item name="imeExtractEnterAnimation">@anim/input_method_extract_enter</item>
- <item name="imeExtractExitAnimation">@anim/input_method_extract_exit</item>
- </style>
-</resources>
diff --git a/core/res/res/xml-watch/default_zen_mode_config.xml b/core/res/res/xml-watch/default_zen_mode_config.xml
new file mode 100644
index 0000000..26af10c
--- /dev/null
+++ b/core/res/res/xml-watch/default_zen_mode_config.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<!-- Default configuration for zen mode. See android.service.notification.ZenModeConfig. -->
+<zen version="2">
+ <!-- Allow starred contacts to go through only. Repeated calls on.
+ Calls, messages, reminders, events off.-->
+ <allow from="2" repeatCallers="true" calls="false" messages="false" reminders="false"
+ events="false"/>
+</zen>
diff --git a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothStressTest.java b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothStressTest.java
index 755e7c4..31ce95e 100644
--- a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothStressTest.java
+++ b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothStressTest.java
@@ -35,6 +35,7 @@
/** The amount of time to sleep between issuing start/stop SCO in ms. */
private static final long SCO_SLEEP_TIME = 2 * 1000;
+ private BluetoothAdapter mAdapter;
private BluetoothTestUtils mTestUtils;
@Override
@@ -42,13 +43,18 @@
super.setUp();
Context context = getInstrumentation().getTargetContext();
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
mTestUtils = new BluetoothTestUtils(context, TAG, OUTPUT_FILE);
+
+ // Start all tests in a disabled state.
+ if (mAdapter.isEnabled()) {
+ mTestUtils.disable(mAdapter);
+ }
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
-
mTestUtils.close();
}
@@ -61,13 +67,10 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mTestUtils.disable(adapter);
-
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("enable iteration " + (i + 1) + " of " + iterations);
- mTestUtils.enable(adapter);
- mTestUtils.disable(adapter);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disable(mAdapter);
}
}
@@ -80,18 +83,14 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.undiscoverable(adapter);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.undiscoverable(mAdapter);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("discoverable iteration " + (i + 1) + " of " + iterations);
- mTestUtils.discoverable(adapter);
- mTestUtils.undiscoverable(adapter);
+ mTestUtils.discoverable(mAdapter);
+ mTestUtils.undiscoverable(mAdapter);
}
-
- mTestUtils.disable(adapter);
}
/**
@@ -103,18 +102,14 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.stopScan(adapter);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.stopScan(mAdapter);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("scan iteration " + (i + 1) + " of " + iterations);
- mTestUtils.startScan(adapter);
- mTestUtils.stopScan(adapter);
+ mTestUtils.startScan(mAdapter);
+ mTestUtils.stopScan(mAdapter);
}
-
- mTestUtils.disable(adapter);
}
/**
@@ -125,19 +120,16 @@
if (iterations == 0) {
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.disablePan(adapter);
+
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disablePan(mAdapter);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("testEnablePan iteration " + (i + 1) + " of "
+ iterations);
- mTestUtils.enablePan(adapter);
- mTestUtils.disablePan(adapter);
+ mTestUtils.enablePan(mAdapter);
+ mTestUtils.disablePan(mAdapter);
}
-
- mTestUtils.disable(adapter);
}
/**
@@ -152,19 +144,16 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("pair iteration " + (i + 1) + " of " + iterations);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.unpair(adapter, device);
+ mTestUtils.unpair(mAdapter, device);
}
- mTestUtils.disable(adapter);
}
/**
@@ -178,19 +167,16 @@
if (iterations == 0) {
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("acceptPair iteration " + (i + 1) + " of " + iterations);
- mTestUtils.acceptPair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ mTestUtils.acceptPair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.unpair(adapter, device);
+ mTestUtils.unpair(mAdapter, device);
}
- mTestUtils.disable(adapter);
}
/**
@@ -205,25 +191,22 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.A2DP, null);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.A2DP, null);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("connectA2dp iteration " + (i + 1) + " of " + iterations);
- mTestUtils.connectProfile(adapter, device, BluetoothProfile.A2DP,
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.A2DP,
String.format("connectA2dp(device=%s)", device));
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.A2DP,
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.A2DP,
String.format("disconnectA2dp(device=%s)", device));
}
- mTestUtils.unpair(adapter, device);
- mTestUtils.disable(adapter);
+ mTestUtils.unpair(mAdapter, device);
}
/**
@@ -238,25 +221,22 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("connectHeadset iteration " + (i + 1) + " of " + iterations);
- mTestUtils.connectProfile(adapter, device, BluetoothProfile.HEADSET,
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.HEADSET,
String.format("connectHeadset(device=%s)", device));
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.HEADSET,
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET,
String.format("disconnectHeadset(device=%s)", device));
}
- mTestUtils.unpair(adapter, device);
- mTestUtils.disable(adapter);
+ mTestUtils.unpair(mAdapter, device);
}
/**
@@ -271,25 +251,22 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.INPUT_DEVICE, null);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.INPUT_DEVICE, null);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("connectInput iteration " + (i + 1) + " of " + iterations);
- mTestUtils.connectProfile(adapter, device, BluetoothProfile.INPUT_DEVICE,
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.INPUT_DEVICE,
String.format("connectInput(device=%s)", device));
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.INPUT_DEVICE,
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.INPUT_DEVICE,
String.format("disconnectInput(device=%s)", device));
}
- mTestUtils.unpair(adapter, device);
- mTestUtils.disable(adapter);
+ mTestUtils.unpair(mAdapter, device);
}
/**
@@ -304,22 +281,19 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("connectPan iteration " + (i + 1) + " of " + iterations);
- mTestUtils.connectPan(adapter, device);
- mTestUtils.disconnectPan(adapter, device);
+ mTestUtils.connectPan(mAdapter, device);
+ mTestUtils.disconnectPan(mAdapter, device);
}
- mTestUtils.unpair(adapter, device);
- mTestUtils.disable(adapter);
+ mTestUtils.unpair(mAdapter, device);
}
/**
@@ -334,26 +308,23 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.disablePan(adapter);
- mTestUtils.enablePan(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.acceptPair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disablePan(mAdapter);
+ mTestUtils.enablePan(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.acceptPair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("incomingPanConnection iteration " + (i + 1) + " of "
+ iterations);
- mTestUtils.incomingPanConnection(adapter, device);
- mTestUtils.incomingPanDisconnection(adapter, device);
+ mTestUtils.incomingPanConnection(mAdapter, device);
+ mTestUtils.incomingPanDisconnection(mAdapter, device);
}
- mTestUtils.unpair(adapter, device);
- mTestUtils.disablePan(adapter);
- mTestUtils.disable(adapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.disablePan(mAdapter);
}
/**
@@ -368,28 +339,25 @@
return;
}
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
- mTestUtils.disable(adapter);
- mTestUtils.enable(adapter);
- mTestUtils.unpair(adapter, device);
- mTestUtils.pair(adapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
BluetoothTestRunner.sDevicePairPin);
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.HEADSET, null);
- mTestUtils.connectProfile(adapter, device, BluetoothProfile.HEADSET, null);
- mTestUtils.stopSco(adapter, device);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.stopSco(mAdapter, device);
for (int i = 0; i < iterations; i++) {
mTestUtils.writeOutput("startStopSco iteration " + (i + 1) + " of " + iterations);
- mTestUtils.startSco(adapter, device);
+ mTestUtils.startSco(mAdapter, device);
sleep(SCO_SLEEP_TIME);
- mTestUtils.stopSco(adapter, device);
+ mTestUtils.stopSco(mAdapter, device);
sleep(SCO_SLEEP_TIME);
}
- mTestUtils.disconnectProfile(adapter, device, BluetoothProfile.HEADSET, null);
- mTestUtils.unpair(adapter, device);
- mTestUtils.disable(adapter);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.unpair(mAdapter, device);
}
private void sleep(long time) {
diff --git a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java
index 0d9980a..ee15978 100644
--- a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java
+++ b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java
@@ -35,6 +35,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
public class BluetoothTestUtils extends Assert {
@@ -423,57 +425,40 @@
* @param adapter The BT adapter.
*/
public void enable(BluetoothAdapter adapter) {
- int mask = (BluetoothReceiver.STATE_TURNING_ON_FLAG | BluetoothReceiver.STATE_ON_FLAG
- | BluetoothReceiver.SCAN_MODE_CONNECTABLE_FLAG);
- long start = -1;
- BluetoothReceiver receiver = getBluetoothReceiver(mask);
-
- int state = adapter.getState();
- switch (state) {
- case BluetoothAdapter.STATE_ON:
- assertTrue(adapter.isEnabled());
- removeReceiver(receiver);
- return;
- case BluetoothAdapter.STATE_TURNING_ON:
- assertFalse(adapter.isEnabled());
- mask = 0; // Don't check for received intents since we might have missed them.
- break;
- case BluetoothAdapter.STATE_OFF:
- assertFalse(adapter.isEnabled());
- start = System.currentTimeMillis();
- assertTrue(adapter.enable());
- break;
- case BluetoothAdapter.STATE_TURNING_OFF:
- start = System.currentTimeMillis();
- assertTrue(adapter.enable());
- break;
- default:
- removeReceiver(receiver);
- fail(String.format("enable() invalid state: state=%d", state));
- }
-
- long s = System.currentTimeMillis();
- while (System.currentTimeMillis() - s < ENABLE_DISABLE_TIMEOUT) {
- state = adapter.getState();
- if (state == BluetoothAdapter.STATE_ON
- && (receiver.getFiredFlags() & mask) == mask) {
- assertTrue(adapter.isEnabled());
- long finish = receiver.getCompletedTime();
- if (start != -1 && finish != -1) {
- writeOutput(String.format("enable() completed in %d ms", (finish - start)));
- } else {
- writeOutput("enable() completed");
+ writeOutput("Enabling Bluetooth adapter.");
+ assertFalse(adapter.isEnabled());
+ int btState = adapter.getState();
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ return;
}
- removeReceiver(receiver);
- return;
+ final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_ON) {
+ completionSemaphore.release();
+ }
}
- sleep(POLL_TIME);
- }
+ };
- int firedFlags = receiver.getFiredFlags();
- removeReceiver(receiver);
- fail(String.format("enable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
- state, BluetoothAdapter.STATE_ON, firedFlags, mask));
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.enable());
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
+ writeOutput(String.format("enable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("enable() timeout: state=%d (expected %d)", btState,
+ BluetoothAdapter.STATE_ON));
+ }
}
/**
@@ -483,57 +468,40 @@
* @param adapter The BT adapter.
*/
public void disable(BluetoothAdapter adapter) {
- int mask = (BluetoothReceiver.STATE_TURNING_OFF_FLAG | BluetoothReceiver.STATE_OFF_FLAG
- | BluetoothReceiver.SCAN_MODE_NONE_FLAG);
- long start = -1;
- BluetoothReceiver receiver = getBluetoothReceiver(mask);
-
- int state = adapter.getState();
- switch (state) {
- case BluetoothAdapter.STATE_OFF:
- assertFalse(adapter.isEnabled());
- removeReceiver(receiver);
- return;
- case BluetoothAdapter.STATE_TURNING_ON:
- assertFalse(adapter.isEnabled());
- start = System.currentTimeMillis();
- break;
- case BluetoothAdapter.STATE_ON:
- assertTrue(adapter.isEnabled());
- start = System.currentTimeMillis();
- assertTrue(adapter.disable());
- break;
- case BluetoothAdapter.STATE_TURNING_OFF:
- assertFalse(adapter.isEnabled());
- mask = 0; // Don't check for received intents since we might have missed them.
- break;
- default:
- removeReceiver(receiver);
- fail(String.format("disable() invalid state: state=%d", state));
- }
-
- long s = System.currentTimeMillis();
- while (System.currentTimeMillis() - s < ENABLE_DISABLE_TIMEOUT) {
- state = adapter.getState();
- if (state == BluetoothAdapter.STATE_OFF
- && (receiver.getFiredFlags() & mask) == mask) {
- assertFalse(adapter.isEnabled());
- long finish = receiver.getCompletedTime();
- if (start != -1 && finish != -1) {
- writeOutput(String.format("disable() completed in %d ms", (finish - start)));
- } else {
- writeOutput("disable() completed");
+ writeOutput("Disabling Bluetooth adapter.");
+ assertTrue(adapter.isEnabled());
+ int btState = adapter.getState();
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ return;
}
- removeReceiver(receiver);
- return;
+ final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_OFF) {
+ completionSemaphore.release();
+ }
}
- sleep(POLL_TIME);
- }
+ };
- int firedFlags = receiver.getFiredFlags();
- removeReceiver(receiver);
- fail(String.format("disable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
- state, BluetoothAdapter.STATE_OFF, firedFlags, mask));
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.disable());
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
+ writeOutput(String.format("disable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("disable() timeout: state=%d (expected %d)", btState,
+ BluetoothAdapter.STATE_OFF));
+ }
}
/**
@@ -543,40 +511,47 @@
* @param adapter The BT adapter.
*/
public void discoverable(BluetoothAdapter adapter) {
- int mask = BluetoothReceiver.SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
-
if (!adapter.isEnabled()) {
fail("discoverable() bluetooth not enabled");
}
int scanMode = adapter.getScanMode();
- if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
return;
}
- BluetoothReceiver receiver = getBluetoothReceiver(mask);
-
- assertEquals(BluetoothAdapter.SCAN_MODE_CONNECTABLE, scanMode);
- long start = System.currentTimeMillis();
- assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
-
- while (System.currentTimeMillis() - start < DISCOVERABLE_UNDISCOVERABLE_TIMEOUT) {
- scanMode = adapter.getScanMode();
- if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
- && (receiver.getFiredFlags() & mask) == mask) {
- writeOutput(String.format("discoverable() completed in %d ms",
- (receiver.getCompletedTime() - start)));
- removeReceiver(receiver);
- return;
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
+ return;
+ }
+ final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_NONE);
+ if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ completionSemaphore.release();
+ }
}
- sleep(POLL_TIME);
- }
+ };
- int firedFlags = receiver.getFiredFlags();
- removeReceiver(receiver);
- fail(String.format("discoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
- + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
- firedFlags, mask));
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ writeOutput(String.format("discoverable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("discoverable() timeout: scanMode=%d (expected %d)", scanMode,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+ }
}
/**
@@ -586,40 +561,47 @@
* @param adapter The BT adapter.
*/
public void undiscoverable(BluetoothAdapter adapter) {
- int mask = BluetoothReceiver.SCAN_MODE_CONNECTABLE_FLAG;
-
if (!adapter.isEnabled()) {
fail("undiscoverable() bluetooth not enabled");
}
int scanMode = adapter.getScanMode();
- if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+ if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
return;
}
- BluetoothReceiver receiver = getBluetoothReceiver(mask);
-
- assertEquals(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, scanMode);
- long start = System.currentTimeMillis();
- assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
-
- while (System.currentTimeMillis() - start < DISCOVERABLE_UNDISCOVERABLE_TIMEOUT) {
- scanMode = adapter.getScanMode();
- if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE
- && (receiver.getFiredFlags() & mask) == mask) {
- writeOutput(String.format("undiscoverable() completed in %d ms",
- (receiver.getCompletedTime() - start)));
- removeReceiver(receiver);
- return;
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
+ return;
+ }
+ final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_NONE);
+ if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+ completionSemaphore.release();
+ }
}
- sleep(POLL_TIME);
- }
+ };
- int firedFlags = receiver.getFiredFlags();
- removeReceiver(receiver);
- fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
- + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE, firedFlags,
- mask));
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ writeOutput(String.format("undiscoverable() completed in 0 ms"));
+ } catch (InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d)", scanMode,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+ }
}
/**
diff --git a/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java
index da8bc1d..7935880 100644
--- a/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java
@@ -45,6 +45,7 @@
private static final int TEST_CMD = 18;
private static final int TEST_ARG1 = 33;
private static final int TEST_ARG2 = 182;
+ private static final Object TEST_OBJ = "hello";
@Mock AlarmManager mAlarmManager;
WakeupMessage mMessage;
@@ -92,7 +93,7 @@
mListenerCaptor.capture(), any(Handler.class));
mMessage = new WakeupMessage(context, mHandler, TEST_CMD_NAME, TEST_CMD, TEST_ARG1,
- TEST_ARG2);
+ TEST_ARG2, TEST_OBJ);
}
/**
@@ -114,6 +115,7 @@
assertEquals("what", TEST_CMD, mHandler.getLastMessage().what);
assertEquals("arg1", TEST_ARG1, mHandler.getLastMessage().arg1);
assertEquals("arg2", TEST_ARG2, mHandler.getLastMessage().arg2);
+ assertEquals("obj", TEST_OBJ, mHandler.getLastMessage().obj);
}
/**
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index 6762bea..0bdc76f 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -46,6 +46,7 @@
import android.util.Log;
import android.util.LongArray;
import android.util.PathParser;
+import android.util.Property;
import android.util.TimeUtils;
import android.view.Choreographer;
import android.view.DisplayListCanvas;
@@ -157,7 +158,7 @@
private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
/** Local, mutable animator set. */
- private VectorDrawableAnimator mAnimatorSet = new VectorDrawableAnimatorUI(this);
+ private VectorDrawableAnimator mAnimatorSet;
/**
* The resources against which this drawable was created. Used to attempt
@@ -182,6 +183,7 @@
private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
+ mAnimatorSet = new VectorDrawableAnimatorRT(this);
mRes = res;
}
@@ -237,6 +239,17 @@
@Override
public void draw(Canvas canvas) {
+ if (!canvas.isHardwareAccelerated() && mAnimatorSet instanceof VectorDrawableAnimatorRT) {
+ // If we have SW canvas and the RT animation is waiting to start, We need to fallback
+ // to UI thread animation for AVD.
+ if (!mAnimatorSet.isRunning() &&
+ ((VectorDrawableAnimatorRT) mAnimatorSet).mPendingAnimationActions.size() > 0) {
+ VectorDrawableAnimatorRT oldAnim = (VectorDrawableAnimatorRT) mAnimatorSet;
+ mAnimatorSet = new VectorDrawableAnimatorUI(this);
+ mAnimatorSet.init(mAnimatorSetFromXml);
+ oldAnim.transferPendingActions(mAnimatorSet);
+ }
+ }
mAnimatorSet.onDraw(canvas);
mAnimatedVectorState.mVectorDrawable.draw(canvas);
}
@@ -390,9 +403,12 @@
R.styleable.AnimatedVectorDrawableTarget_animation, 0);
if (animResId != 0) {
if (theme != null) {
- final Animator objectAnimator = AnimatorInflater.loadAnimator(
+ // The animator here could be ObjectAnimator or AnimatorSet.
+ final Animator animator = AnimatorInflater.loadAnimator(
res, theme, animResId, pathErrorScale);
- state.addTargetAnimator(target, objectAnimator);
+ updateAnimatorProperty(animator, target, state.mVectorDrawable,
+ state.mShouldIgnoreInvalidAnim);
+ state.addTargetAnimator(target, animator);
} else {
// The animation may be theme-dependent. As a
// workaround until Animator has full support for
@@ -414,6 +430,55 @@
mRes = state.mPendingAnims == null ? null : res;
}
+ private static void updateAnimatorProperty(Animator animator, String targetName,
+ VectorDrawable vectorDrawable, boolean ignoreInvalidAnim) {
+ if (animator instanceof ObjectAnimator) {
+ // Change the property of the Animator from using reflection based on the property
+ // name to a Property object that wraps the setter and getter for modifying that
+ // specific property for a given object. By replacing the reflection with a direct call,
+ // we can largely reduce the time it takes for a animator to modify a VD property.
+ PropertyValuesHolder[] holders = ((ObjectAnimator) animator).getValues();
+ for (int i = 0; i < holders.length; i++) {
+ PropertyValuesHolder pvh = holders[i];
+ String propertyName = pvh.getPropertyName();
+ Object targetNameObj = vectorDrawable.getTargetByName(targetName);
+ Property property = null;
+ if (targetNameObj instanceof VectorDrawable.VObject) {
+ property = ((VectorDrawable.VObject) targetNameObj).getProperty(propertyName);
+ } else if (targetNameObj instanceof VectorDrawable.VectorDrawableState) {
+ property = ((VectorDrawable.VectorDrawableState) targetNameObj)
+ .getProperty(propertyName);
+ }
+ if (property != null) {
+ if (containsSameValueType(pvh, property)) {
+ pvh.setProperty(property);
+ } else if (!ignoreInvalidAnim) {
+ throw new RuntimeException("Wrong valueType for Property: " + propertyName
+ + ". Expected type: " + property.getType().toString() + ". Actual "
+ + "type defined in resources: " + pvh.getValueType().toString());
+
+ }
+ }
+ }
+ } else if (animator instanceof AnimatorSet) {
+ for (Animator anim : ((AnimatorSet) animator).getChildAnimations()) {
+ updateAnimatorProperty(anim, targetName, vectorDrawable, ignoreInvalidAnim);
+ }
+ }
+ }
+
+ private static boolean containsSameValueType(PropertyValuesHolder holder, Property property) {
+ Class type1 = holder.getValueType();
+ Class type2 = property.getType();
+ if (type1 == float.class || type1 == Float.class) {
+ return type2 == float.class || type2 == Float.class;
+ } else if (type1 == int.class || type1 == Integer.class) {
+ return type2 == int.class || type2 == Integer.class;
+ } else {
+ return type1 == type2;
+ }
+ }
+
/**
* Force to animate on UI thread.
* @hide
@@ -462,6 +527,8 @@
@Config int mChangingConfigurations;
VectorDrawable mVectorDrawable;
+ private final boolean mShouldIgnoreInvalidAnim;
+
/** Animators that require a theme before inflation. */
ArrayList<PendingAnimator> mPendingAnims;
@@ -473,6 +540,7 @@
public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
Callback owner, Resources res) {
+ mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
if (copy != null) {
mChangingConfigurations = copy.mChangingConfigurations;
@@ -616,8 +684,10 @@
for (int i = 0, count = pendingAnims.size(); i < count; i++) {
final PendingAnimator pendingAnimator = pendingAnims.get(i);
- final Animator objectAnimator = pendingAnimator.newInstance(res, t);
- addTargetAnimator(pendingAnimator.target, objectAnimator);
+ final Animator animator = pendingAnimator.newInstance(res, t);
+ updateAnimatorProperty(animator, pendingAnimator.target, mVectorDrawable,
+ mShouldIgnoreInvalidAnim);
+ addTargetAnimator(pendingAnimator.target, animator);
}
}
}
@@ -982,6 +1052,9 @@
private static final int REVERSE_ANIMATION = 2;
private static final int RESET_ANIMATION = 3;
private static final int END_ANIMATION = 4;
+
+ // If the duration of an animation is more than 300 frames, we cap the sample size to 300.
+ private static final int MAX_SAMPLE_POINTS = 300;
private AnimatorListener mListener = null;
private final LongArray mStartDelays = new LongArray();
private PropertyValuesHolder.PropertyValues mTmpValues =
@@ -992,14 +1065,12 @@
private boolean mInitialized = false;
private boolean mIsReversible = false;
private boolean mIsInfinite = false;
- // This needs to be set before parsing starts.
- private boolean mShouldIgnoreInvalidAnim;
// TODO: Consider using NativeAllocationRegistery to track native allocation
private final VirtualRefBasePtr mSetRefBasePtr;
private WeakReference<RenderNode> mLastSeenTarget = null;
private int mLastListenerId = 0;
private final IntArray mPendingAnimationActions = new IntArray();
- private final Drawable mDrawable;
+ private final AnimatedVectorDrawable mDrawable;
VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) {
mDrawable = drawable;
@@ -1016,8 +1087,10 @@
throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
"re-initialized");
}
- mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
parseAnimatorSet(set, 0);
+ long vectorDrawableTreePtr = mDrawable.mAnimatedVectorState.mVectorDrawable
+ .getNativeTree();
+ nSetVectorDrawableTarget(mSetPtr, vectorDrawableTreePtr);
mInitialized = true;
mIsInfinite = set.getTotalDuration() == Animator.DURATION_INFINITE;
@@ -1077,7 +1150,7 @@
} else if (target instanceof VectorDrawable.VFullPath) {
createRTAnimatorForFullPath(animator, (VectorDrawable.VFullPath) target,
startTime);
- } else if (!mShouldIgnoreInvalidAnim) {
+ } else if (!mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
throw new IllegalArgumentException("ClipPath only supports PathData " +
"property");
}
@@ -1086,7 +1159,7 @@
} else if (target instanceof VectorDrawable.VectorDrawableState) {
createRTAnimatorForRootGroup(values, animator,
(VectorDrawable.VectorDrawableState) target, startTime);
- } else if (!mShouldIgnoreInvalidAnim) {
+ } else if (!mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
// Should never get here
throw new UnsupportedOperationException("Target should be either VGroup, VPath, " +
"or ConstantState, " + target == null ? "Null target" : target.getClass() +
@@ -1121,8 +1194,8 @@
long propertyPtr = nCreateGroupPropertyHolder(nativePtr, propertyId,
(Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
if (mTmpValues.dataSource != null) {
- float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator
- .getDuration());
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
}
createNativeChildAnimator(propertyPtr, startTime, animator);
@@ -1149,7 +1222,7 @@
long nativePtr = target.getNativePtr();
if (mTmpValues.type == Float.class || mTmpValues.type == float.class) {
if (propertyId < 0) {
- if (mShouldIgnoreInvalidAnim) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
return;
} else {
throw new IllegalArgumentException("Property: " + mTmpValues.propertyName
@@ -1158,12 +1231,24 @@
}
propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId,
(Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
} else if (mTmpValues.type == Integer.class || mTmpValues.type == int.class) {
propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId,
(Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ int[] dataPoints = createIntDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
} else {
- if (mShouldIgnoreInvalidAnim) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
return;
} else {
throw new UnsupportedOperationException("Unsupported type: " +
@@ -1171,45 +1256,63 @@
"supported for Paths.");
}
}
- if (mTmpValues.dataSource != null) {
- float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator
- .getDuration());
- nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
- }
createNativeChildAnimator(propertyPtr, startTime, animator);
}
private void createRTAnimatorForRootGroup(PropertyValuesHolder[] values,
ObjectAnimator animator, VectorDrawable.VectorDrawableState target,
long startTime) {
- long nativePtr = target.getNativeRenderer();
- if (!animator.getPropertyName().equals("alpha")) {
- if (mShouldIgnoreInvalidAnim) {
- return;
- } else {
- throw new UnsupportedOperationException("Only alpha is supported for root "
- + "group");
- }
+ long nativePtr = target.getNativeRenderer();
+ if (!animator.getPropertyName().equals("alpha")) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("Only alpha is supported for root "
+ + "group");
}
- Float startValue = null;
- Float endValue = null;
- for (int i = 0; i < values.length; i++) {
- values[i].getPropertyValues(mTmpValues);
- if (mTmpValues.propertyName.equals("alpha")) {
- startValue = (Float) mTmpValues.startValue;
- endValue = (Float) mTmpValues.endValue;
- break;
- }
+ }
+ Float startValue = null;
+ Float endValue = null;
+ for (int i = 0; i < values.length; i++) {
+ values[i].getPropertyValues(mTmpValues);
+ if (mTmpValues.propertyName.equals("alpha")) {
+ startValue = (Float) mTmpValues.startValue;
+ endValue = (Float) mTmpValues.endValue;
+ break;
}
- if (startValue == null && endValue == null) {
- if (mShouldIgnoreInvalidAnim) {
- return;
- } else {
- throw new UnsupportedOperationException("No alpha values are specified");
- }
+ }
+ if (startValue == null && endValue == null) {
+ if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("No alpha values are specified");
}
- long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue);
- createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+ long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue);
+ if (mTmpValues.dataSource != null) {
+ // Pass keyframe data to native, if any.
+ float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource,
+ animator.getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ /**
+ * Calculate the amount of frames an animation will run based on duration.
+ */
+ private static int getFrameCount(long duration) {
+ long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
+ int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
+ int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs);
+ // We need 2 frames of data minimum.
+ numAnimFrames = Math.max(2, numAnimFrames);
+ if (numAnimFrames > MAX_SAMPLE_POINTS) {
+ Log.w("AnimatedVectorDrawable", "Duration for the animation is too long :" +
+ duration + ", the animation will subsample the keyframe or path data.");
+ numAnimFrames = MAX_SAMPLE_POINTS;
+ }
+ return numAnimFrames;
}
// These are the data points that define the value of the animating properties.
@@ -1217,11 +1320,9 @@
// a point on the path corresponds to the values of translateX and translateY.
// TODO: (Optimization) We should pass the path down in native and chop it into segments
// in native.
- private static float[] createDataPoints(
+ private static float[] createFloatDataPoints(
PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) {
- long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
- int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
- int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs);
+ int numAnimFrames = getFrameCount(duration);
float values[] = new float[numAnimFrames];
float lastFrame = numAnimFrames - 1;
for (int i = 0; i < numAnimFrames; i++) {
@@ -1231,6 +1332,18 @@
return values;
}
+ private static int[] createIntDataPoints(
+ PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) {
+ int numAnimFrames = getFrameCount(duration);
+ int values[] = new int[numAnimFrames];
+ float lastFrame = numAnimFrames - 1;
+ for (int i = 0; i < numAnimFrames; i++) {
+ float fraction = i / lastFrame;
+ values[i] = (Integer) dataSource.getValueAtFraction(fraction);
+ }
+ return values;
+ }
+
private void createNativeChildAnimator(long propertyPtr, long extraDelay,
ObjectAnimator animator) {
long duration = animator.getDuration();
@@ -1254,16 +1367,19 @@
* to the last seen RenderNode target and start right away.
*/
protected void recordLastSeenTarget(DisplayListCanvas canvas) {
- mLastSeenTarget = new WeakReference<RenderNode>(
- RenderNodeAnimatorSetHelper.getTarget(canvas));
- if (mPendingAnimationActions.size() > 0 && useLastSeenTarget()) {
- if (DBG_ANIMATION_VECTOR_DRAWABLE) {
- Log.d(LOGTAG, "Target is set in the next frame");
+ final RenderNode node = RenderNodeAnimatorSetHelper.getTarget(canvas);
+ mLastSeenTarget = new WeakReference<RenderNode>(node);
+ // Add the animator to the list of animators on every draw
+ if (mInitialized || mPendingAnimationActions.size() > 0) {
+ if (useTarget(node)) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set in the next frame");
+ }
+ for (int i = 0; i < mPendingAnimationActions.size(); i++) {
+ handlePendingAction(mPendingAnimationActions.get(i));
+ }
+ mPendingAnimationActions.clear();
}
- for (int i = 0; i < mPendingAnimationActions.size(); i++) {
- handlePendingAction(mPendingAnimationActions.get(i));
- }
- mPendingAnimationActions.clear();
}
}
@@ -1285,10 +1401,15 @@
private boolean useLastSeenTarget() {
if (mLastSeenTarget != null) {
final RenderNode target = mLastSeenTarget.get();
- if (target != null && target.isAttached()) {
- target.addAnimator(this);
- return true;
- }
+ return useTarget(target);
+ }
+ return false;
+ }
+
+ private boolean useTarget(RenderNode target) {
+ if (target != null && target.isAttached()) {
+ target.registerVectorDrawableAnimator(this);
+ return true;
}
return false;
}
@@ -1480,9 +1601,29 @@
private static void callOnFinished(VectorDrawableAnimatorRT set, int id) {
set.onAnimationEnd(id);
}
+
+ private void transferPendingActions(VectorDrawableAnimator animatorSet) {
+ for (int i = 0; i < mPendingAnimationActions.size(); i++) {
+ int pendingAction = mPendingAnimationActions.get(i);
+ if (pendingAction == START_ANIMATION) {
+ animatorSet.start();
+ } else if (pendingAction == END_ANIMATION) {
+ animatorSet.end();
+ } else if (pendingAction == REVERSE_ANIMATION) {
+ animatorSet.reverse();
+ } else if (pendingAction == RESET_ANIMATION) {
+ animatorSet.reset();
+ } else {
+ throw new UnsupportedOperationException("Animation action " +
+ pendingAction + "is not supported");
+ }
+ }
+ mPendingAnimationActions.clear();
+ }
}
private static native long nCreateAnimatorSet();
+ private static native void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr);
private static native void nAddAnimator(long setPtr, long propertyValuesHolder,
long nativeInterpolator, long startDelay, long duration, int repeatCount);
@@ -1498,6 +1639,7 @@
private static native long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue,
float endValue);
private static native void nSetPropertyHolderData(long nativePtr, float[] data, int length);
+ private static native void nSetPropertyHolderData(long nativePtr, int[] data, int length);
private static native void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
private static native void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
private static native void nEnd(long animatorSetPtr);
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index f5592fa..a8c8737 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -34,9 +34,12 @@
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
+import android.util.FloatProperty;
+import android.util.IntProperty;
import android.util.LayoutDirection;
import android.util.Log;
import android.util.PathParser;
+import android.util.Property;
import android.util.Xml;
import com.android.internal.R;
@@ -758,6 +761,13 @@
return mVectorState.mAutoMirrored;
}
+ /**
+ * @hide
+ */
+ public long getNativeTree() {
+ return mVectorState.getNativeRenderer();
+ }
+
static class VectorDrawableState extends ConstantState {
// Variables below need to be copied (deep copy if applicable) for mutation.
int[] mThemeAttrs;
@@ -790,6 +800,26 @@
int mLastSWCachePixelCount = 0;
int mLastHWCachePixelCount = 0;
+ final static Property<VectorDrawableState, Float> ALPHA =
+ new FloatProperty<VectorDrawableState>("alpha") {
+ @Override
+ public void setValue(VectorDrawableState state, float value) {
+ state.setAlpha(value);
+ }
+
+ @Override
+ public Float get(VectorDrawableState state) {
+ return state.getAlpha();
+ }
+ };
+
+ Property getProperty(String propertyName) {
+ if (ALPHA.getName().equals(propertyName)) {
+ return ALPHA;
+ }
+ return null;
+ }
+
// This tracks the total native allocation for all the nodes.
private int mAllocationOfAllNodes = 0;
@@ -967,7 +997,7 @@
}
static class VGroup extends VObject {
- private static final int ROTATE_INDEX = 0;
+ private static final int ROTATION_INDEX = 0;
private static final int PIVOT_X_INDEX = 1;
private static final int PIVOT_Y_INDEX = 2;
private static final int SCALE_X_INDEX = 3;
@@ -978,7 +1008,7 @@
private static final int NATIVE_ALLOCATION_SIZE = 100;
- private static final HashMap<String, Integer> sPropertyMap =
+ private static final HashMap<String, Integer> sPropertyIndexMap =
new HashMap<String, Integer>() {
{
put("translateX", TRANSLATE_X_INDEX);
@@ -987,19 +1017,123 @@
put("scaleY", SCALE_Y_INDEX);
put("pivotX", PIVOT_X_INDEX);
put("pivotY", PIVOT_Y_INDEX);
- put("rotation", ROTATE_INDEX);
+ put("rotation", ROTATION_INDEX);
}
};
static int getPropertyIndex(String propertyName) {
- if (sPropertyMap.containsKey(propertyName)) {
- return sPropertyMap.get(propertyName);
+ if (sPropertyIndexMap.containsKey(propertyName)) {
+ return sPropertyIndexMap.get(propertyName);
} else {
// property not found
return -1;
}
}
+ // Below are the Properties that wrap the setters to avoid reflection overhead in animations
+ private static final Property<VGroup, Float> TRANSLATE_X =
+ new FloatProperty<VGroup> ("translateX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setTranslateX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getTranslateX();
+ }
+ };
+
+ private static final Property<VGroup, Float> TRANSLATE_Y =
+ new FloatProperty<VGroup> ("translateY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setTranslateY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getTranslateY();
+ }
+ };
+
+ private static final Property<VGroup, Float> SCALE_X =
+ new FloatProperty<VGroup> ("scaleX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setScaleX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getScaleX();
+ }
+ };
+
+ private static final Property<VGroup, Float> SCALE_Y =
+ new FloatProperty<VGroup> ("scaleY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setScaleY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getScaleY();
+ }
+ };
+
+ private static final Property<VGroup, Float> PIVOT_X =
+ new FloatProperty<VGroup> ("pivotX") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setPivotX(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getPivotX();
+ }
+ };
+
+ private static final Property<VGroup, Float> PIVOT_Y =
+ new FloatProperty<VGroup> ("pivotY") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setPivotY(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getPivotY();
+ }
+ };
+
+ private static final Property<VGroup, Float> ROTATION =
+ new FloatProperty<VGroup> ("rotation") {
+ @Override
+ public void setValue(VGroup object, float value) {
+ object.setRotation(value);
+ }
+
+ @Override
+ public Float get(VGroup object) {
+ return object.getRotation();
+ }
+ };
+
+ private static final HashMap<String, Property> sPropertyMap =
+ new HashMap<String, Property>() {
+ {
+ put("translateX", TRANSLATE_X);
+ put("translateY", TRANSLATE_Y);
+ put("scaleX", SCALE_X);
+ put("scaleY", SCALE_Y);
+ put("pivotX", PIVOT_X);
+ put("pivotY", PIVOT_Y);
+ put("rotation", ROTATION);
+ }
+ };
// Temp array to store transform values obtained from native.
private float[] mTransform;
/////////////////////////////////////////////////////
@@ -1055,6 +1189,15 @@
mNativePtr = nCreateGroup();
}
+ Property getProperty(String propertyName) {
+ if (sPropertyMap.containsKey(propertyName)) {
+ return sPropertyMap.get(propertyName);
+ } else {
+ // property not found
+ return null;
+ }
+ }
+
public String getGroupName() {
return mGroupName;
}
@@ -1102,7 +1245,7 @@
throw new RuntimeException("Error: inconsistent property count");
}
float rotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation,
- mTransform[ROTATE_INDEX]);
+ mTransform[ROTATION_INDEX]);
float pivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX,
mTransform[PIVOT_X_INDEX]);
float pivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY,
@@ -1288,6 +1431,27 @@
String mPathName;
@Config int mChangingConfigurations;
+ private static final Property<VPath, PathParser.PathData> PATH_DATA =
+ new Property<VPath, PathParser.PathData>(PathParser.PathData.class, "pathData") {
+ @Override
+ public void set(VPath object, PathParser.PathData data) {
+ object.setPathData(data);
+ }
+
+ @Override
+ public PathParser.PathData get(VPath object) {
+ return object.getPathData();
+ }
+ };
+
+ Property getProperty(String propertyName) {
+ if (PATH_DATA.getName().equals(propertyName)) {
+ return PATH_DATA;
+ }
+ // property not found
+ return null;
+ }
+
public VPath() {
// Empty constructor.
}
@@ -1410,7 +1574,7 @@
private static final int NATIVE_ALLOCATION_SIZE = 264;
// Property map for animatable attributes.
- private final static HashMap<String, Integer> sPropertyMap
+ private final static HashMap<String, Integer> sPropertyIndexMap
= new HashMap<String, Integer> () {
{
put("strokeWidth", STROKE_WIDTH_INDEX);
@@ -1424,6 +1588,125 @@
}
};
+ // Below are the Properties that wrap the setters to avoid reflection overhead in animations
+ private static final Property<VFullPath, Float> STROKE_WIDTH =
+ new FloatProperty<VFullPath> ("strokeWidth") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setStrokeWidth(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getStrokeWidth();
+ }
+ };
+
+ private static final Property<VFullPath, Integer> STROKE_COLOR =
+ new IntProperty<VFullPath> ("strokeColor") {
+ @Override
+ public void setValue(VFullPath object, int value) {
+ object.setStrokeColor(value);
+ }
+
+ @Override
+ public Integer get(VFullPath object) {
+ return object.getStrokeColor();
+ }
+ };
+
+ private static final Property<VFullPath, Float> STROKE_ALPHA =
+ new FloatProperty<VFullPath> ("strokeAlpha") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setStrokeAlpha(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getStrokeAlpha();
+ }
+ };
+
+ private static final Property<VFullPath, Integer> FILL_COLOR =
+ new IntProperty<VFullPath>("fillColor") {
+ @Override
+ public void setValue(VFullPath object, int value) {
+ object.setFillColor(value);
+ }
+
+ @Override
+ public Integer get(VFullPath object) {
+ return object.getFillColor();
+ }
+ };
+
+ private static final Property<VFullPath, Float> FILL_ALPHA =
+ new FloatProperty<VFullPath> ("fillAlpha") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setFillAlpha(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getFillAlpha();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_START =
+ new FloatProperty<VFullPath> ("trimPathStart") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathStart(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathStart();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_END =
+ new FloatProperty<VFullPath> ("trimPathEnd") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathEnd(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathEnd();
+ }
+ };
+
+ private static final Property<VFullPath, Float> TRIM_PATH_OFFSET =
+ new FloatProperty<VFullPath> ("trimPathOffset") {
+ @Override
+ public void setValue(VFullPath object, float value) {
+ object.setTrimPathOffset(value);
+ }
+
+ @Override
+ public Float get(VFullPath object) {
+ return object.getTrimPathOffset();
+ }
+ };
+
+ private final static HashMap<String, Property> sPropertyMap
+ = new HashMap<String, Property> () {
+ {
+ put("strokeWidth", STROKE_WIDTH);
+ put("strokeColor", STROKE_COLOR);
+ put("strokeAlpha", STROKE_ALPHA);
+ put("fillColor", FILL_COLOR);
+ put("fillAlpha", FILL_ALPHA);
+ put("trimPathStart", TRIM_PATH_START);
+ put("trimPathEnd", TRIM_PATH_END);
+ put("trimPathOffset", TRIM_PATH_OFFSET);
+ }
+ };
+
// Temp array to store property data obtained from native getter.
private byte[] mPropertyData;
/////////////////////////////////////////////////////
@@ -1446,11 +1729,24 @@
mFillColors = copy.mFillColors;
}
+ Property getProperty(String propertyName) {
+ Property p = super.getProperty(propertyName);
+ if (p != null) {
+ return p;
+ }
+ if (sPropertyMap.containsKey(propertyName)) {
+ return sPropertyMap.get(propertyName);
+ } else {
+ // property not found
+ return null;
+ }
+ }
+
int getPropertyIndex(String propertyName) {
- if (!sPropertyMap.containsKey(propertyName)) {
+ if (!sPropertyIndexMap.containsKey(propertyName)) {
return -1;
} else {
- return sPropertyMap.get(propertyName);
+ return sPropertyIndexMap.get(propertyName);
}
}
@@ -1784,6 +2080,7 @@
abstract boolean onStateChange(int[] state);
abstract boolean isStateful();
abstract int getNativeSize();
+ abstract Property getProperty(String propertyName);
}
private static native long nCreateTree(long rootGroupPtr);
diff --git a/libs/hwui/AnimationContext.h b/libs/hwui/AnimationContext.h
index 909ed36..801fd87 100644
--- a/libs/hwui/AnimationContext.h
+++ b/libs/hwui/AnimationContext.h
@@ -100,6 +100,8 @@
ANDROID_API virtual void destroy();
+ ANDROID_API virtual void detachAnimators() {}
+
private:
friend class AnimationHandle;
void addAnimationHandle(AnimationHandle* handle);
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index 4d65782..dc18018 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -274,6 +274,10 @@
return playTime >= mDuration;
}
+nsecs_t BaseRenderNodeAnimator::getRemainingPlayTime() {
+ return mPlayState == PlayState::Reversing ? mPlayTime : mDuration - mPlayTime;
+}
+
void BaseRenderNodeAnimator::forceEndNow(AnimationContext& context) {
if (mPlayState < PlayState::Finished) {
mPlayState = PlayState::Finished;
diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h
index fdae0f3..9476750 100644
--- a/libs/hwui/Animator.h
+++ b/libs/hwui/Animator.h
@@ -62,19 +62,23 @@
}
bool mayRunAsync() { return mMayRunAsync; }
ANDROID_API void start();
- ANDROID_API void reset();
+ ANDROID_API virtual void reset();
ANDROID_API void reverse();
// Terminates the animation at its current progress.
ANDROID_API void cancel();
// Terminates the animation and skip to the end of the animation.
- ANDROID_API void end();
+ ANDROID_API virtual void end();
void attach(RenderNode* target);
virtual void onAttached() {}
void detach() { mTarget = nullptr; }
- void pushStaging(AnimationContext& context);
- bool animate(AnimationContext& context);
+ ANDROID_API void pushStaging(AnimationContext& context);
+ ANDROID_API bool animate(AnimationContext& context);
+
+ // Returns the remaining time in ms for the animation. Note this should only be called during
+ // an animation on RenderThread.
+ ANDROID_API nsecs_t getRemainingPlayTime();
bool isRunning() { return mPlayState == PlayState::Running
|| mPlayState == PlayState::Reversing; }
diff --git a/libs/hwui/DisplayList.cpp b/libs/hwui/DisplayList.cpp
index b572bda..28be05c 100644
--- a/libs/hwui/DisplayList.cpp
+++ b/libs/hwui/DisplayList.cpp
@@ -45,7 +45,7 @@
, regions(stdAllocator)
, referenceHolders(stdAllocator)
, functors(stdAllocator)
- , pushStagingFunctors(stdAllocator)
+ , vectorDrawables(stdAllocator)
, hasDrawOps(false) {
}
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index 5b3227b..ccf71c6 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -69,6 +69,11 @@
typedef DrawRenderNodeOp NodeOpType;
#endif
+namespace VectorDrawable {
+class Tree;
+};
+typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot;
+
/**
* Holds data used in the playback a tree of DisplayLists.
*/
@@ -110,16 +115,6 @@
LinearAllocator mReplayAllocator;
};
-/**
- * Functor that can be used for objects with data in both UI thread and RT to keep the data
- * in sync. This functor, when added to DisplayList, will be call during DisplayList sync.
- */
-struct PushStagingFunctor {
- PushStagingFunctor() {}
- virtual ~PushStagingFunctor() {}
- virtual void operator ()() {}
-};
-
struct FunctorContainer {
Functor* functor;
GlFunctorLifecycleListener* listener;
@@ -161,7 +156,7 @@
const LsaVector<const SkBitmap*>& getBitmapResources() const { return bitmapResources; }
const LsaVector<FunctorContainer>& getFunctors() const { return functors; }
- const LsaVector<PushStagingFunctor*>& getPushStagingFunctors() { return pushStagingFunctors; }
+ const LsaVector<VectorDrawableRoot*>& getVectorDrawables() { return vectorDrawables; }
size_t addChild(NodeOpType* childOp);
@@ -203,10 +198,10 @@
// List of functors
LsaVector<FunctorContainer> functors;
- // List of functors that need to be notified of pushStaging. Note that this list gets nothing
+ // List of VectorDrawables that need to be notified of pushStaging. Note that this list gets nothing
// but a callback during sync DisplayList, unlike the list of functors defined above, which
// gets special treatment exclusive for webview.
- LsaVector<PushStagingFunctor*> pushStagingFunctors;
+ LsaVector<VectorDrawableRoot*> vectorDrawables;
bool hasDrawOps; // only used if !HWUI_NEW_OPS
diff --git a/libs/hwui/DisplayListCanvas.cpp b/libs/hwui/DisplayListCanvas.cpp
index ca968ce..bec66295 100644
--- a/libs/hwui/DisplayListCanvas.cpp
+++ b/libs/hwui/DisplayListCanvas.cpp
@@ -417,7 +417,7 @@
void DisplayListCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
mDisplayList->ref(tree);
- mDisplayList->pushStagingFunctors.push_back(tree->getFunctor());
+ mDisplayList->vectorDrawables.push_back(tree);
addDrawOp(new (alloc()) DrawVectorDrawableOp(tree, tree->stagingProperties()->getBounds()));
}
diff --git a/libs/hwui/PropertyValuesAnimatorSet.cpp b/libs/hwui/PropertyValuesAnimatorSet.cpp
index b29f91f..e416e0c 100644
--- a/libs/hwui/PropertyValuesAnimatorSet.cpp
+++ b/libs/hwui/PropertyValuesAnimatorSet.cpp
@@ -30,6 +30,11 @@
interpolator, startDelay, duration, repeatCount);
mAnimators.emplace_back(animator);
setListener(new PropertyAnimatorSetListener(this));
+
+ // Check whether any child animator is infinite after adding it them to the set.
+ if (repeatCount == -1) {
+ mIsInfinite = true;
+ }
}
PropertyValuesAnimatorSet::PropertyValuesAnimatorSet()
@@ -78,15 +83,27 @@
void PropertyValuesAnimatorSet::start(AnimationListener* listener) {
init();
mOneShotListener = listener;
+ mRequestId++;
BaseRenderNodeAnimator::start();
}
void PropertyValuesAnimatorSet::reverse(AnimationListener* listener) {
init();
mOneShotListener = listener;
+ mRequestId++;
BaseRenderNodeAnimator::reverse();
}
+void PropertyValuesAnimatorSet::reset() {
+ mRequestId++;
+ BaseRenderNodeAnimator::reset();
+}
+
+void PropertyValuesAnimatorSet::end() {
+ mRequestId++;
+ BaseRenderNodeAnimator::end();
+}
+
void PropertyValuesAnimatorSet::init() {
if (mInitialized) {
return;
diff --git a/libs/hwui/PropertyValuesAnimatorSet.h b/libs/hwui/PropertyValuesAnimatorSet.h
index c7ae7c0..49021bc 100644
--- a/libs/hwui/PropertyValuesAnimatorSet.h
+++ b/libs/hwui/PropertyValuesAnimatorSet.h
@@ -43,6 +43,7 @@
float mLatestFraction = 0.0f;
};
+// TODO: This class should really be named VectorDrawableAnimator
class ANDROID_API PropertyValuesAnimatorSet : public BaseRenderNodeAnimator {
public:
friend class PropertyAnimatorSetListener;
@@ -50,11 +51,19 @@
void start(AnimationListener* listener);
void reverse(AnimationListener* listener);
+ virtual void reset() override;
+ virtual void end() override;
void addPropertyAnimator(PropertyValuesHolder* propertyValuesHolder,
Interpolator* interpolators, int64_t startDelays,
nsecs_t durations, int repeatCount);
virtual uint32_t dirtyMask();
+ bool isInfinite() { return mIsInfinite; }
+ void setVectorDrawable(VectorDrawableRoot* vd) { mVectorDrawable = vd; }
+ VectorDrawableRoot* getVectorDrawable() const { return mVectorDrawable; }
+ AnimationListener* getOneShotListener() { return mOneShotListener.get(); }
+ void clearOneShotListener() { mOneShotListener = nullptr; }
+ uint32_t getRequestId() const { return mRequestId; }
protected:
virtual float getValue(RenderNode* target) const override;
@@ -69,6 +78,11 @@
std::vector< std::unique_ptr<PropertyAnimator> > mAnimators;
float mLastFraction = 0.0f;
bool mInitialized = false;
+ VectorDrawableRoot* mVectorDrawable = nullptr;
+ bool mIsInfinite = false;
+ // This request id gets incremented (on UI thread only) when a new request to modfiy the
+ // lifecycle of an animation happens, namely when start/end/reset/reverse is called.
+ uint32_t mRequestId = 0;
};
class PropertyAnimatorSetListener : public AnimationListener {
diff --git a/libs/hwui/PropertyValuesHolder.cpp b/libs/hwui/PropertyValuesHolder.cpp
index 0932d65..6ba0ab5 100644
--- a/libs/hwui/PropertyValuesHolder.cpp
+++ b/libs/hwui/PropertyValuesHolder.cpp
@@ -25,7 +25,27 @@
using namespace VectorDrawable;
-float PropertyValuesHolder::getValueFromData(float fraction) {
+inline U8CPU lerp(U8CPU fromValue, U8CPU toValue, float fraction) {
+ return (U8CPU) (fromValue * (1 - fraction) + toValue * fraction);
+}
+
+// TODO: Add a test for this
+void ColorEvaluator::evaluate(SkColor* outColor,
+ const SkColor& fromColor, const SkColor& toColor, float fraction) const {
+ U8CPU alpha = lerp(SkColorGetA(fromColor), SkColorGetA(toColor), fraction);
+ U8CPU red = lerp(SkColorGetR(fromColor), SkColorGetR(toColor), fraction);
+ U8CPU green = lerp(SkColorGetG(fromColor), SkColorGetG(toColor), fraction);
+ U8CPU blue = lerp(SkColorGetB(fromColor), SkColorGetB(toColor), fraction);
+ *outColor = SkColorSetARGB(alpha, red, green, blue);
+}
+
+void PathEvaluator::evaluate(PathData* out,
+ const PathData& from, const PathData& to, float fraction) const {
+ VectorDrawableUtils::interpolatePaths(out, from, to, fraction);
+}
+
+template<typename T>
+const T PropertyValuesHolderImpl<T>::getValueFromData(float fraction) const {
if (mDataSource.size() == 0) {
LOG_ALWAYS_FATAL("No data source is defined");
return 0;
@@ -41,57 +61,44 @@
int lowIndex = floor(fraction);
fraction -= lowIndex;
- float value = mDataSource[lowIndex] * (1.0f - fraction)
- + mDataSource[lowIndex + 1] * fraction;
+ T value;
+ mEvaluator->evaluate(&value, mDataSource[lowIndex], mDataSource[lowIndex + 1], fraction);
return value;
}
-void GroupPropertyValuesHolder::setFraction(float fraction) {
- float animatedValue;
+template<typename T>
+const T PropertyValuesHolderImpl<T>::calculateAnimatedValue(float fraction) const {
if (mDataSource.size() > 0) {
- animatedValue = getValueFromData(fraction);
+ return getValueFromData(fraction);
} else {
- animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
+ T value;
+ mEvaluator->evaluate(&value, mStartValue, mEndValue, fraction);
+ return value;
}
+}
+
+void GroupPropertyValuesHolder::setFraction(float fraction) {
+ float animatedValue = calculateAnimatedValue(fraction);
mGroup->mutateProperties()->setPropertyValue(mPropertyId, animatedValue);
}
-inline U8CPU lerp(U8CPU fromValue, U8CPU toValue, float fraction) {
- return (U8CPU) (fromValue * (1 - fraction) + toValue * fraction);
-}
-
-// TODO: Add a test for this
-SkColor FullPathColorPropertyValuesHolder::interpolateColors(SkColor fromColor, SkColor toColor,
- float fraction) {
- U8CPU alpha = lerp(SkColorGetA(fromColor), SkColorGetA(toColor), fraction);
- U8CPU red = lerp(SkColorGetR(fromColor), SkColorGetR(toColor), fraction);
- U8CPU green = lerp(SkColorGetG(fromColor), SkColorGetG(toColor), fraction);
- U8CPU blue = lerp(SkColorGetB(fromColor), SkColorGetB(toColor), fraction);
- return SkColorSetARGB(alpha, red, green, blue);
-}
-
void FullPathColorPropertyValuesHolder::setFraction(float fraction) {
- SkColor animatedValue = interpolateColors(mStartValue, mEndValue, fraction);
+ SkColor animatedValue = calculateAnimatedValue(fraction);
mFullPath->mutateProperties()->setColorPropertyValue(mPropertyId, animatedValue);
}
void FullPathPropertyValuesHolder::setFraction(float fraction) {
- float animatedValue;
- if (mDataSource.size() > 0) {
- animatedValue = getValueFromData(fraction);
- } else {
- animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
- }
+ float animatedValue = calculateAnimatedValue(fraction);
mFullPath->mutateProperties()->setPropertyValue(mPropertyId, animatedValue);
}
void PathDataPropertyValuesHolder::setFraction(float fraction) {
- VectorDrawableUtils::interpolatePaths(&mPathData, mStartValue, mEndValue, fraction);
+ mEvaluator->evaluate(&mPathData, mStartValue, mEndValue, fraction);
mPath->mutateProperties()->setData(mPathData);
}
void RootAlphaPropertyValuesHolder::setFraction(float fraction) {
- float animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
+ float animatedValue = calculateAnimatedValue(fraction);
mTree->mutateProperties()->setRootAlpha(animatedValue);
}
diff --git a/libs/hwui/PropertyValuesHolder.h b/libs/hwui/PropertyValuesHolder.h
index b905fae..432f8ba 100644
--- a/libs/hwui/PropertyValuesHolder.h
+++ b/libs/hwui/PropertyValuesHolder.h
@@ -31,91 +31,130 @@
class ANDROID_API PropertyValuesHolder {
public:
virtual void setFraction(float fraction) = 0;
- void setPropertyDataSource(float* dataSource, int length) {
- mDataSource.insert(mDataSource.begin(), dataSource, dataSource + length);
- }
- float getValueFromData(float fraction);
- virtual ~PropertyValuesHolder() {}
-protected:
- std::vector<float> mDataSource;
+ virtual ~PropertyValuesHolder() {}
};
-class ANDROID_API GroupPropertyValuesHolder : public PropertyValuesHolder {
+template <typename T>
+class Evaluator {
+public:
+ virtual void evaluate(T* out, const T& from, const T& to, float fraction) const {};
+ virtual ~Evaluator() {}
+};
+
+class FloatEvaluator : public Evaluator<float> {
+public:
+ virtual void evaluate(float* out, const float& from, const float& to, float fraction)
+ const override {
+ *out = from * (1 - fraction) + to * fraction;
+ }
+};
+
+class ANDROID_API ColorEvaluator : public Evaluator<SkColor> {
+public:
+ virtual void evaluate(SkColor* outColor, const SkColor& from, const SkColor& to,
+ float fraction) const override;
+};
+
+class ANDROID_API PathEvaluator : public Evaluator<PathData> {
+ virtual void evaluate(PathData* out, const PathData& from, const PathData& to, float fraction)
+ const override;
+};
+
+template <typename T>
+class ANDROID_API PropertyValuesHolderImpl : public PropertyValuesHolder {
+public:
+ PropertyValuesHolderImpl(const T& startValue, const T& endValue)
+ : mStartValue(startValue)
+ , mEndValue(endValue) {}
+ void setPropertyDataSource(T* dataSource, int length) {
+ mDataSource.insert(mDataSource.begin(), dataSource, dataSource + length);
+ }
+ // Calculate the animated value from the data source.
+ const T getValueFromData(float fraction) const;
+ // Convenient method to favor getting animated value from data source. If no data source is set
+ // fall back to linear interpolation.
+ const T calculateAnimatedValue(float fraction) const;
+protected:
+ std::unique_ptr<Evaluator<T>> mEvaluator = nullptr;
+ // This contains uniformly sampled data throughout the animation duration. The first element
+ // should be the start value and the last should be the end value of the animation. When the
+ // data source is set, we'll favor data source over the linear interpolation of start/end value
+ // for calculation of animated value.
+ std::vector<T> mDataSource;
+ T mStartValue;
+ T mEndValue;
+};
+
+class ANDROID_API GroupPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
GroupPropertyValuesHolder(VectorDrawable::Group* ptr, int propertyId, float startValue,
float endValue)
- : mGroup(ptr)
- , mPropertyId(propertyId)
- , mStartValue(startValue)
- , mEndValue(endValue){
+ : PropertyValuesHolderImpl(startValue, endValue)
+ , mGroup(ptr)
+ , mPropertyId(propertyId) {
+ mEvaluator.reset(new FloatEvaluator());
}
void setFraction(float fraction) override;
private:
VectorDrawable::Group* mGroup;
int mPropertyId;
- float mStartValue;
- float mEndValue;
};
-class ANDROID_API FullPathColorPropertyValuesHolder : public PropertyValuesHolder {
+class ANDROID_API FullPathColorPropertyValuesHolder : public PropertyValuesHolderImpl<SkColor> {
public:
- FullPathColorPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, int32_t startValue,
- int32_t endValue)
- : mFullPath(ptr)
- , mPropertyId(propertyId)
- , mStartValue(startValue)
- , mEndValue(endValue) {};
+ FullPathColorPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId,
+ SkColor startValue, SkColor endValue)
+ : PropertyValuesHolderImpl(startValue, endValue)
+ , mFullPath(ptr)
+ , mPropertyId(propertyId) {
+ mEvaluator.reset(new ColorEvaluator());
+ }
void setFraction(float fraction) override;
static SkColor interpolateColors(SkColor fromColor, SkColor toColor, float fraction);
private:
VectorDrawable::FullPath* mFullPath;
int mPropertyId;
- int32_t mStartValue;
- int32_t mEndValue;
};
-class ANDROID_API FullPathPropertyValuesHolder : public PropertyValuesHolder {
+class ANDROID_API FullPathPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
FullPathPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, float startValue,
float endValue)
- : mFullPath(ptr)
- , mPropertyId(propertyId)
- , mStartValue(startValue)
- , mEndValue(endValue) {};
+ : PropertyValuesHolderImpl(startValue, endValue)
+ , mFullPath(ptr)
+ , mPropertyId(propertyId) {
+ mEvaluator.reset(new FloatEvaluator());
+ };
void setFraction(float fraction) override;
private:
VectorDrawable::FullPath* mFullPath;
int mPropertyId;
- float mStartValue;
- float mEndValue;
};
-class ANDROID_API PathDataPropertyValuesHolder : public PropertyValuesHolder {
+class ANDROID_API PathDataPropertyValuesHolder : public PropertyValuesHolderImpl<PathData> {
public:
PathDataPropertyValuesHolder(VectorDrawable::Path* ptr, PathData* startValue,
PathData* endValue)
- : mPath(ptr)
- , mStartValue(*startValue)
- , mEndValue(*endValue) {};
+ : PropertyValuesHolderImpl(*startValue, *endValue)
+ , mPath(ptr) {
+ mEvaluator.reset(new PathEvaluator());
+ };
void setFraction(float fraction) override;
private:
VectorDrawable::Path* mPath;
PathData mPathData;
- PathData mStartValue;
- PathData mEndValue;
};
-class ANDROID_API RootAlphaPropertyValuesHolder : public PropertyValuesHolder {
+class ANDROID_API RootAlphaPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
RootAlphaPropertyValuesHolder(VectorDrawable::Tree* tree, float startValue, float endValue)
- : mTree(tree)
- , mStartValue(startValue)
- , mEndValue(endValue) {}
+ : PropertyValuesHolderImpl(startValue, endValue)
+ , mTree(tree) {
+ mEvaluator.reset(new FloatEvaluator());
+ }
void setFraction(float fraction) override;
private:
VectorDrawable::Tree* mTree;
- float mStartValue;
- float mEndValue;
};
}
}
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index b49f9b5..b35c926 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -440,8 +440,8 @@
}
void RecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
- mDisplayList->pushStagingFunctors.push_back(tree->getFunctor());
mDisplayList->ref(tree);
+ mDisplayList->vectorDrawables.push_back(tree);
addOp(alloc().create_trivial<VectorDrawableOp>(
tree,
Rect(tree->stagingProperties()->getBounds()),
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index d48d544..f8797bf 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -421,6 +421,16 @@
prepareSubTree(info, childFunctorsNeedLayer, mDisplayList);
pushLayerUpdate(info);
+ if (mDisplayList) {
+ for (auto& vectorDrawable : mDisplayList->getVectorDrawables()) {
+ // If any vector drawable in the display list needs update, damage the node.
+ if (vectorDrawable->isDirty()) {
+ damageSelf(info);
+ }
+ vectorDrawable->setPropertyChangeWillBeConsumed(true);
+ }
+ }
+
info.damageAccumulator->popTransform();
}
@@ -479,8 +489,8 @@
for (auto& iter : mDisplayList->getFunctors()) {
(*iter.functor)(DrawGlInfo::kModeSync, nullptr);
}
- for (size_t i = 0; i < mDisplayList->getPushStagingFunctors().size(); i++) {
- (*mDisplayList->getPushStagingFunctors()[i])();
+ for (auto& vectorDrawable : mDisplayList->getVectorDrawables()) {
+ vectorDrawable->syncProperties();
}
}
}
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index a5d1d4b..e67dfdd 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -673,21 +673,16 @@
void onPropertyChanged(TreeProperties* prop);
TreeProperties* mutateStagingProperties() { return &mStagingProperties; }
const TreeProperties* stagingProperties() const { return &mStagingProperties; }
- PushStagingFunctor* getFunctor() { return &mFunctor;}
// This should only be called from animations on RT
TreeProperties* mutateProperties() { return &mProperties; }
+ // This should always be called from RT.
+ bool isDirty() const { return mCache.dirty; }
+ bool getPropertyChangeWillBeConsumed() const { return mWillBeConsumed; }
+ void setPropertyChangeWillBeConsumed(bool willBeConsumed) { mWillBeConsumed = willBeConsumed; }
+
private:
- class VectorDrawableFunctor : public PushStagingFunctor {
- public:
- VectorDrawableFunctor(Tree* tree) : mTree(tree) {}
- virtual void operator ()() {
- mTree->syncProperties();
- }
- private:
- Tree* mTree;
- };
SkPaint* updatePaint(SkPaint* outPaint, TreeProperties* prop);
bool allocateBitmapIfNeeded(SkBitmap* outCache, int width, int height);
@@ -704,8 +699,6 @@
TreeProperties mProperties = TreeProperties(this);
TreeProperties mStagingProperties = TreeProperties(this);
- VectorDrawableFunctor mFunctor = VectorDrawableFunctor(this);
-
SkPaint mPaint;
struct Cache {
SkBitmap bitmap;
@@ -717,6 +710,8 @@
PropertyChangedListener mPropertyChangedListener
= PropertyChangedListener(&mCache.dirty, &mStagingCache.dirty);
+
+ mutable bool mWillBeConsumed = false;
};
} // namespace VectorDrawable
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index c626c54..d4956be 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -282,6 +282,7 @@
void CanvasContext::stopDrawing() {
mRenderThread.removeFrameCallback(this);
+ mAnimationContext->detachAnimators();
}
void CanvasContext::notifyFramePending() {
diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp
index b2997df..cf76a86 100644
--- a/libs/hwui/tests/unit/RenderNodeTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeTests.cpp
@@ -16,13 +16,26 @@
#include <gtest/gtest.h>
+#include "AnimationContext.h"
+#include "DamageAccumulator.h"
+#include "IContextFactory.h"
#include "RenderNode.h"
#include "TreeInfo.h"
+#include "renderthread/CanvasContext.h"
#include "tests/common/TestUtils.h"
#include "utils/Color.h"
using namespace android;
using namespace android::uirenderer;
+using namespace android::uirenderer::renderthread;
+
+class ContextFactory : public android::uirenderer::IContextFactory {
+public:
+ android::uirenderer::AnimationContext* createAnimationContext
+ (android::uirenderer::renderthread::TimeLord& clock) override {
+ return new android::uirenderer::AnimationContext(clock);
+ }
+};
TEST(RenderNode, hasParents) {
auto child = TestUtils::createNode(0, 0, 200, 400,
@@ -89,3 +102,31 @@
TestUtils::syncHierarchyPropertiesAndDisplayList(node);
EXPECT_EQ(0, refcnt);
}
+
+RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) {
+ ContextFactory contextFactory;
+ CanvasContext canvasContext(renderThread, false, nullptr, &contextFactory);
+ TreeInfo info(TreeInfo::MODE_RT_ONLY, canvasContext);
+ DamageAccumulator damageAccumulator;
+ info.damageAccumulator = &damageAccumulator;
+ info.observer = nullptr;
+
+ {
+ auto nonNullDLNode = TestUtils::createNode(0, 0, 200, 400,
+ [](RenderProperties& props, TestCanvas& canvas) {
+ canvas.drawColor(Color::Red_500, SkXfermode::kSrcOver_Mode);
+ });
+ TestUtils::syncHierarchyPropertiesAndDisplayList(nonNullDLNode);
+ EXPECT_TRUE(nonNullDLNode->getDisplayList());
+ nonNullDLNode->prepareTree(info);
+ }
+
+ {
+ auto nullDLNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
+ TestUtils::syncHierarchyPropertiesAndDisplayList(nullDLNode);
+ EXPECT_FALSE(nullDLNode->getDisplayList());
+ nullDLNode->prepareTree(info);
+ }
+
+ canvasContext.destroy(nullptr);
+}
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_cellphone.png
deleted file mode 100644
index 6e29d23..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_headphones_a2dp.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_headphones_a2dp.png
deleted file mode 100644
index 6110e9e..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_headphones_a2dp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_headset_hfp.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_headset_hfp.png
deleted file mode 100644
index 6cca225..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_headset_hfp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_misc_hid.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_misc_hid.png
deleted file mode 100644
index 6445f2a..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_misc_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_network_pan.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_network_pan.png
deleted file mode 100644
index 78c0294..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_network_pan.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_bt_pointing_hid.png b/packages/SettingsLib/res/drawable-hdpi/ic_bt_pointing_hid.png
deleted file mode 100644
index 2fcc3b0..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_bt_pointing_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-hdpi/ic_lockscreen_ime.png b/packages/SettingsLib/res/drawable-hdpi/ic_lockscreen_ime.png
deleted file mode 100644
index 70d35bf..0000000
--- a/packages/SettingsLib/res/drawable-hdpi/ic_lockscreen_ime.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-ldrtl-hdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-ldrtl-hdpi/ic_bt_cellphone.png
deleted file mode 100644
index 2d9b75e..0000000
--- a/packages/SettingsLib/res/drawable-ldrtl-hdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-ldrtl-mdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-ldrtl-mdpi/ic_bt_cellphone.png
deleted file mode 100644
index b6ebe34..0000000
--- a/packages/SettingsLib/res/drawable-ldrtl-mdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-ldrtl-xhdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-ldrtl-xhdpi/ic_bt_cellphone.png
deleted file mode 100644
index 8b67b91..0000000
--- a/packages/SettingsLib/res/drawable-ldrtl-xhdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_cellphone.png
deleted file mode 100644
index 1fa0a3d..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_headphones_a2dp.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_headphones_a2dp.png
deleted file mode 100644
index 175bd78..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_headphones_a2dp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_headset_hfp.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_headset_hfp.png
deleted file mode 100644
index 05b27e8..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_headset_hfp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_misc_hid.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_misc_hid.png
deleted file mode 100644
index 6e9f8ae..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_misc_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_network_pan.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_network_pan.png
deleted file mode 100644
index 5f3371f..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_network_pan.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_bt_pointing_hid.png b/packages/SettingsLib/res/drawable-mdpi/ic_bt_pointing_hid.png
deleted file mode 100644
index 539d77f..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_bt_pointing_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-mdpi/ic_lockscreen_ime.png b/packages/SettingsLib/res/drawable-mdpi/ic_lockscreen_ime.png
deleted file mode 100644
index 3216776..0000000
--- a/packages/SettingsLib/res/drawable-mdpi/ic_lockscreen_ime.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_cellphone.png
deleted file mode 100644
index 4f381ba..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headphones_a2dp.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headphones_a2dp.png
deleted file mode 100644
index c67127d..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headphones_a2dp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headset_hfp.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headset_hfp.png
deleted file mode 100644
index d3b356b..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_headset_hfp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_misc_hid.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_misc_hid.png
deleted file mode 100644
index 2d38129..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_misc_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_network_pan.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_network_pan.png
deleted file mode 100644
index fb76575..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_network_pan.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_pointing_hid.png b/packages/SettingsLib/res/drawable-xhdpi/ic_bt_pointing_hid.png
deleted file mode 100644
index d8b68eb..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_bt_pointing_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xhdpi/ic_lockscreen_ime.png b/packages/SettingsLib/res/drawable-xhdpi/ic_lockscreen_ime.png
deleted file mode 100644
index 02cc3af..0000000
--- a/packages/SettingsLib/res/drawable-xhdpi/ic_lockscreen_ime.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_cellphone.png
deleted file mode 100644
index 7805b7a..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headphones_a2dp.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headphones_a2dp.png
deleted file mode 100644
index 8127774..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headphones_a2dp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headset_hfp.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headset_hfp.png
deleted file mode 100644
index 84b8085..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_headset_hfp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_misc_hid.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_misc_hid.png
deleted file mode 100644
index 289d6ac..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_misc_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_network_pan.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_network_pan.png
deleted file mode 100644
index 72bc804..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_network_pan.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_pointing_hid.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_pointing_hid.png
deleted file mode 100644
index e31ce2b..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_bt_pointing_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxhdpi/ic_lockscreen_ime.png b/packages/SettingsLib/res/drawable-xxhdpi/ic_lockscreen_ime.png
deleted file mode 100644
index f23b0e7..0000000
--- a/packages/SettingsLib/res/drawable-xxhdpi/ic_lockscreen_ime.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_cellphone.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_cellphone.png
deleted file mode 100644
index 1e12f96..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_cellphone.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headphones_a2dp.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headphones_a2dp.png
deleted file mode 100644
index 8b547d9..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headphones_a2dp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headset_hfp.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headset_hfp.png
deleted file mode 100644
index 03c5033..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_headset_hfp.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_misc_hid.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_misc_hid.png
deleted file mode 100644
index b9a9923..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_misc_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_network_pan.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_network_pan.png
deleted file mode 100644
index 989e1ab..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_network_pan.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_pointing_hid.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_pointing_hid.png
deleted file mode 100644
index de8c389..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_bt_pointing_hid.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable-xxxhdpi/ic_lockscreen_ime.png b/packages/SettingsLib/res/drawable-xxxhdpi/ic_lockscreen_ime.png
deleted file mode 100644
index 2eb8a92..0000000
--- a/packages/SettingsLib/res/drawable-xxxhdpi/ic_lockscreen_ime.png
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/res/drawable/ic_bt_cellphone.xml b/packages/SettingsLib/res/drawable/ic_bt_cellphone.xml
new file mode 100644
index 0000000..cc9b732
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_cellphone.xml
@@ -0,0 +1,29 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36
+ 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1
+ -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45
+ 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_bt_headphones_a2dp.xml b/packages/SettingsLib/res/drawable/ic_bt_headphones_a2dp.xml
new file mode 100644
index 0000000..bbe3914
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_headphones_a2dp.xml
@@ -0,0 +1,26 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87
+ 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_bt_headset_hfp.xml b/packages/SettingsLib/res/drawable/ic_bt_headset_hfp.xml
new file mode 100644
index 0000000..ceeef19
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_headset_hfp.xml
@@ -0,0 +1,27 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87
+ 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h4v1h-7v2h6c1.66,0 3,-1.34 3,-3V10c0,-4.97 -4.03,-9
+ -9,-9z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_bt_misc_hid.xml b/packages/SettingsLib/res/drawable/ic_bt_misc_hid.xml
new file mode 100644
index 0000000..67b42ae
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_misc_hid.xml
@@ -0,0 +1,26 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3
+ -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_bt_network_pan.xml b/packages/SettingsLib/res/drawable/ic_bt_network_pan.xml
new file mode 100644
index 0000000..c5ab01c
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_network_pan.xml
@@ -0,0 +1,30 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82
+ -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57
+ 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89
+ -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19
+ 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29
+ 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_bt_pointing_hid.xml b/packages/SettingsLib/res/drawable/ic_bt_pointing_hid.xml
new file mode 100644
index 0000000..821618d
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_pointing_hid.xml
@@ -0,0 +1,26 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M13,1.07L13,9h7c0,-4.08 -3.05,-7.44 -7,-7.93zM4,15c0,4.42
+ 3.58,8 8,8s8,-3.58 8,-8v-4L4,11v4zM11,1.07C7.05,1.56 4,4.92 4,9h7L11,1.07z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/ic_lockscreen_ime.xml b/packages/SettingsLib/res/drawable/ic_lockscreen_ime.xml
new file mode 100644
index 0000000..4aa8569
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_lockscreen_ime.xml
@@ -0,0 +1,26 @@
+<!--
+ 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:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9
+ 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM11,8h2v2h-2L11,8zM11,11h2v2h-2v-2zM8,8h2v2L8,10L8,8zM8,11h2v2L8,13v-2zM7,13L5,13v-2h2v2zM7,10L5,10L5,8h2v2zM16,17L8,17v-2h8v2zM16,13h-2v-2h2v2zM16,10h-2L14,8h2v2zM19,13h-2v-2h2v2zM19,10h-2L17,8h2v2z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/layout/settings_with_drawer.xml b/packages/SettingsLib/res/layout/settings_with_drawer.xml
index 67296a6..a68a44e 100644
--- a/packages/SettingsLib/res/layout/settings_with_drawer.xml
+++ b/packages/SettingsLib/res/layout/settings_with_drawer.xml
@@ -41,6 +41,11 @@
android:background="?android:attr/colorPrimary" />
</FrameLayout>
<FrameLayout
+ android:id="@+id/content_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="?android:attr/actionBarStyle" />
+ <FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="fill_parent"
diff --git a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
index 320cd58..5791168 100644
--- a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
@@ -24,6 +24,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
@@ -156,16 +157,15 @@
return intent;
}
- private static void addIntentParameters(Context context, Intent intent, String backupContext) {
+ public static void addIntentParameters(Context context, Intent intent, String backupContext) {
if (!intent.hasExtra(EXTRA_CONTEXT)) {
// Insert some context if none exists.
intent.putExtra(EXTRA_CONTEXT, backupContext);
}
intent.putExtra(EXTRA_THEME, 1 /* Light, dark action bar */);
- Theme theme = context.getTheme();
- TypedValue typedValue = new TypedValue();
- theme.resolveAttribute(android.R.attr.colorPrimary, typedValue, true);
- intent.putExtra(EXTRA_PRIMARY_COLOR, context.getColor(typedValue.resourceId));
+ TypedArray array = context.obtainStyledAttributes(new int[]{android.R.attr.colorPrimary});
+ intent.putExtra(EXTRA_PRIMARY_COLOR, array.getColor(0, 0));
+ array.recycle();
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
index 37e3c53..6658c14 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
@@ -41,8 +41,10 @@
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.widget.AdapterView;
+import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.Toolbar;
+
import com.android.settingslib.R;
import com.android.settingslib.applications.InterestingConfigChanges;
@@ -68,6 +70,7 @@
private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
private SettingsDrawerAdapter mDrawerAdapter;
+ private FrameLayout mContentHeaderContainer;
private DrawerLayout mDrawerLayout;
private boolean mShowingMenu;
@@ -84,6 +87,7 @@
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
super.setContentView(R.layout.settings_with_drawer);
+ mContentHeaderContainer = (FrameLayout) findViewById(R.id.content_header_container);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
if (mDrawerLayout == null) {
return;
@@ -180,6 +184,13 @@
}
}
+ public void setContentHeaderView(View headerView) {
+ mContentHeaderContainer.removeAllViews();
+ if (headerView != null) {
+ mContentHeaderContainer.addView(headerView);
+ }
+ }
+
@Override
public void setContentView(@LayoutRes int layoutResID) {
final ViewGroup parent = (ViewGroup) findViewById(R.id.content_frame);
@@ -272,6 +283,13 @@
}
}
+ public HashMap<Pair<String, String>, Tile> getTileCache() {
+ if (sTileCache == null) {
+ getDashboardCategories();
+ }
+ return sTileCache;
+ }
+
public void onProfileTileOpen() {
finish();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
index 418b138..b9c758c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
@@ -302,7 +302,7 @@
return false;
}
- private static final Comparator<Tile> TILE_COMPARATOR =
+ public static final Comparator<Tile> TILE_COMPARATOR =
new Comparator<Tile>() {
@Override
public int compare(Tile lhs, Tile rhs) {
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 978ca94..108814e 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -216,4 +216,7 @@
<!-- Default setting for ability to add users from the lock screen -->
<bool name="def_add_users_from_lockscreen">false</bool>
+
+ <!-- Default setting for disallow oem unlock. -->
+ <bool name="def_oem_unlock_disallow">false</bool>
</resources>
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 774be60..950c7d3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2074,7 +2074,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 127;
+ private static final int SETTINGS_VERSION = 128;
private final int mUserId;
@@ -2329,6 +2329,18 @@
currentVersion = 127;
}
+ if (currentVersion == 127) {
+ // Version 127: Disable OEM unlock setting by default on some devices.
+ final SettingsState globalSettings = getGlobalSettingsLocked();
+ String defaultOemUnlockDisabled = (getContext().getResources()
+ .getBoolean(R.bool.def_oem_unlock_disallow) ? "1" : "0");
+ globalSettings.insertSettingLocked(
+ Settings.Global.OEM_UNLOCK_DISALLOWED,
+ defaultOemUnlockDisabled,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ currentVersion = 128;
+ }
+
// vXXX: Add new settings above this point.
// Return the current version.
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 25dc357..809774b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -165,6 +165,9 @@
<!-- the ability to rename notifications posted by other apps -->
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
+ <!-- shortcut manager -->
+ <uses-permission android:name="android.permission.RESET_SHORTCUT_MANAGER_THROTTLING" />
+
<application
android:name=".SystemUIApplication"
android:persistent="true"
diff --git a/packages/SystemUI/res/drawable/ic_mode_edit.xml b/packages/SystemUI/res/drawable/ic_mode_edit.xml
new file mode 100644
index 0000000..8a73686
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_mode_edit.xml
@@ -0,0 +1,24 @@
+<!--
+ 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:width="20.0dp"
+ android:height="20.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M3.0,17.25L3.0,21.0l3.75,0.0L17.81,9.94l-3.75,-3.75L3.0,17.25zM20.71,7.04c0.39,-0.3 0.39,-1.02 0.0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0.0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_qs_branded_vpn.xml b/packages/SystemUI/res/drawable/ic_qs_branded_vpn.xml
new file mode 100644
index 0000000..736a04a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_branded_vpn.xml
@@ -0,0 +1,24 @@
+<?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:width="12.0dp"
+ android:height="12.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#4DFFFFFF"
+ android:pathData="M12.700000,10.000000c-0.800000,-2.300000 -3.000000,-4.000000 -5.700000,-4.000000c-3.300000,0.000000 -6.000000,2.700000 -6.000000,6.000000s2.700000,6.000000 6.000000,6.000000c2.600000,0.000000 4.800000,-1.700000 5.700000,-4.000000L17.000000,14.000000l0.000000,4.000000l4.000000,0.000000l0.000000,-4.000000l2.000000,0.000000l0.000000,-4.000000L12.700000,10.000000zM7.000000,14.000000c-1.100000,0.000000 -2.000000,-0.900000 -2.000000,-2.000000c0.000000,-1.100000 0.900000,-2.000000 2.000000,-2.000000s2.000000,0.900000 2.000000,2.000000C9.000000,13.100000 8.100000,14.000000 7.000000,14.000000z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_settings_20dp.xml b/packages/SystemUI/res/drawable/ic_settings_20dp.xml
new file mode 100644
index 0000000..3170f86
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_settings_20dp.xml
@@ -0,0 +1,24 @@
+<!--
+ 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:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19.4,13.0c0.0,-0.3 0.1,-0.6 0.1,-1.0s0.0,-0.7 -0.1,-1.0l2.1,-1.7c0.2,-0.2 0.2,-0.4 0.1,-0.6l-2.0,-3.5C19.5,5.1 19.3,5.0 19.0,5.1l-2.5,1.0c-0.5,-0.4 -1.1,-0.7 -1.7,-1.0l-0.4,-2.6C14.5,2.2 14.2,2.0 14.0,2.0l-4.0,0.0C9.8,2.0 9.5,2.2 9.5,2.4L9.1,5.1C8.5,5.3 8.0,5.7 7.4,6.1L5.0,5.1C4.7,5.0 4.5,5.1 4.3,5.3l-2.0,3.5C2.2,8.9 2.3,9.2 2.5,9.4L4.6,11.0c0.0,0.3 -0.1,0.6 -0.1,1.0s0.0,0.7 0.1,1.0l-2.1,1.7c-0.2,0.2 -0.2,0.4 -0.1,0.6l2.0,3.5C4.5,18.9 4.7,19.0 5.0,18.9l2.5,-1.0c0.5,0.4 1.1,0.7 1.7,1.0l0.4,2.6c0.0,0.2 0.2,0.4 0.5,0.4l4.0,0.0c0.2,0.0 0.5,-0.2 0.5,-0.4l0.4,-2.6c0.6,-0.3 1.2,-0.6 1.7,-1.0l2.5,1.0c0.2,0.1 0.5,0.0 0.6,-0.2l2.0,-3.5c0.1,-0.2 0.1,-0.5 -0.1,-0.6L19.4,13.0zM12.0,15.5c-1.9,0.0 -3.5,-1.6 -3.5,-3.5s1.6,-3.5 3.5,-3.5s3.5,1.6 3.5,3.5S13.9,15.5 12.0,15.5z"
+ android:fillColor="#ffffffff" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/stat_sys_branded_vpn.xml b/packages/SystemUI/res/drawable/stat_sys_branded_vpn.xml
new file mode 100644
index 0000000..a86e5b9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_branded_vpn.xml
@@ -0,0 +1,24 @@
+<?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:width="17.0dp"
+ android:height="17.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M12.700000,10.000000c-0.800000,-2.300000 -3.000000,-4.000000 -5.700000,-4.000000c-3.300000,0.000000 -6.000000,2.700000 -6.000000,6.000000s2.700000,6.000000 6.000000,6.000000c2.600000,0.000000 4.800000,-1.700000 5.700000,-4.000000L17.000000,14.000000l0.000000,4.000000l4.000000,0.000000l0.000000,-4.000000l2.000000,0.000000l0.000000,-4.000000L12.700000,10.000000zM7.000000,14.000000c-1.100000,0.000000 -2.000000,-0.900000 -2.000000,-2.000000c0.000000,-1.100000 0.900000,-2.000000 2.000000,-2.000000s2.000000,0.900000 2.000000,2.000000C9.000000,13.100000 8.100000,14.000000 7.000000,14.000000z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/qs_paged_tile_layout.xml b/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
index ee55ec2..8b6060f 100644
--- a/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
+++ b/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
@@ -37,19 +37,6 @@
android:importantForAccessibility="yes"
android:focusable="true" />
- <TextView
- android:id="@android:id/edit"
- style="@style/QSBorderlessButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="end"
- android:minWidth="88dp"
- android:textAppearance="@style/TextAppearance.QS.DetailButton"
- android:textColor="#64FFFFFF"
- android:focusable="true"
- android:text="@string/qs_edit"
- android:contentDescription="@string/accessibility_quick_settings_edit"/>
-
</FrameLayout>
</com.android.systemui.qs.PagedTileLayout>
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index 751f181..26c7339 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -26,7 +26,7 @@
<com.android.systemui.qs.QSPanel
android:id="@+id/quick_settings_panel"
android:background="#0000"
- android:layout_marginTop="@dimen/status_bar_header_height"
+ android:layout_marginTop="80dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp" />
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index f65a667..103e0b0 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -37,7 +37,7 @@
android:clipToPadding="false"
android:orientation="horizontal"
android:layout_alignParentEnd="true"
- android:layout_marginTop="28dp"
+ android:layout_marginTop="4dp"
android:layout_marginEnd="12dp">
<com.android.systemui.statusbar.phone.MultiUserSwitch android:id="@+id/multi_user_switch"
@@ -52,6 +52,18 @@
android:scaleType="centerInside"/>
</com.android.systemui.statusbar.phone.MultiUserSwitch>
+ <com.android.systemui.statusbar.AlphaOptimizedImageView
+ android:id="@android:id/edit"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:clipToPadding="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/ic_mode_edit"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/accessibility_quick_settings_edit"
+ android:padding="14dp" />
+
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/settings_button_container"
android:layout_width="48dp"
@@ -64,7 +76,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ripple_drawable"
- android:src="@drawable/ic_settings"
+ android:src="@drawable/ic_settings_20dp"
android:contentDescription="@string/accessibility_quick_settings_settings" />
<com.android.systemui.statusbar.AlphaOptimizedImageView android:id="@+id/tuner_icon"
android:layout_width="match_parent"
@@ -98,7 +110,7 @@
android:layout_alignParentTop="true"
android:paddingStart="16dp"
android:paddingEnd="16dp"
- android:paddingTop="8dp"
+ android:paddingTop="2dp"
android:visibility="gone"
android:textAppearance="@style/TextAppearance.StatusBar.Expanded.EmergencyCallsOnly"
android:text="@*android:string/emergency_calls_only"
@@ -109,6 +121,7 @@
<include
android:id="@+id/date_time_alarm_group"
layout="@layout/status_bar_alarm_group"
+ android:layout_marginTop="16dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
@@ -116,7 +129,7 @@
android:id="@+id/quick_qs_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="28dp"
+ android:layout_marginTop="52dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_alignParentEnd="true"
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 6c5a313..7e63cbf 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -94,8 +94,6 @@
<dimen name="navigation_key_width">128dp</dimen>
<dimen name="navigation_key_padding">25dp</dimen>
- <dimen name="qs_expand_margin">0dp</dimen>
-
<!-- Keyboard shortcuts helper -->
<dimen name="ksh_layout_width">488dp</dimen>
diff --git a/packages/SystemUI/res/values-w550dp-land/dimens.xml b/packages/SystemUI/res/values-w550dp-land/dimens.xml
index 4160c83..eaca9d7 100644
--- a/packages/SystemUI/res/values-w550dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-w550dp-land/dimens.xml
@@ -18,6 +18,4 @@
<resources>
<!-- Standard notification width + gravity -->
<dimen name="notification_panel_width">544dp</dimen>
-
- <dimen name="qs_expand_margin">32dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 02b1b50..134388f 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -103,9 +103,19 @@
wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location
</string>
+ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
+ <string name="quick_settings_tiles_stock" translatable="false">
+ wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,work,cast,night
+ </string>
+
<!-- The tiles to display in QuickSettings -->
<string name="quick_settings_tiles" translatable="false">default</string>
+ <!-- The tiles to display in QuickSettings in retail mode -->
+ <string name="quick_settings_tiles_retail_mode" translatable="false">
+ cell,battery,dnd,flashlight,rotation,location
+ </string>
+
<!-- Whether or not the RSSI tile is capitalized or not. -->
<bool name="quick_settings_rssi_tile_capitalization">true</bool>
@@ -265,4 +275,3 @@
<bool name="quick_settings_show_full_alarm">false</bool>
</resources>
-
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2a4752a..eef5717 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -130,7 +130,7 @@
<dimen name="close_handle_underlap">32dp</dimen>
<!-- Height of the status bar header bar -->
- <dimen name="status_bar_header_height">80dp</dimen>
+ <dimen name="status_bar_header_height">104dp</dimen>
<!-- Height of the status bar header bar when expanded -->
<dimen name="status_bar_header_height_expanded">116dp</dimen>
@@ -172,8 +172,6 @@
<dimen name="qs_tile_margin_top">16dp</dimen>
<dimen name="qs_quick_tile_size">48dp</dimen>
<dimen name="qs_quick_tile_padding">12dp</dimen>
- <dimen name="qs_date_anim_translation">32dp</dimen>
- <dimen name="qs_date_alarm_anim_translation">22dp</dimen>
<dimen name="qs_date_collapsed_text_size">14sp</dimen>
<dimen name="qs_date_text_size">16sp</dimen>
<dimen name="qs_header_gear_translation">16dp</dimen>
@@ -202,7 +200,6 @@
<dimen name="qs_detail_margin_top">28dp</dimen>
<dimen name="qs_data_usage_text_size">14sp</dimen>
<dimen name="qs_data_usage_usage_text_size">36sp</dimen>
- <dimen name="qs_expand_margin">0dp</dimen>
<dimen name="qs_battery_padding">2dp</dimen>
<dimen name="qs_detail_items_padding_top">4dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/Interpolators.java b/packages/SystemUI/src/com/android/systemui/Interpolators.java
index 17f4dab..fd3bd92 100644
--- a/packages/SystemUI/src/com/android/systemui/Interpolators.java
+++ b/packages/SystemUI/src/com/android/systemui/Interpolators.java
@@ -36,6 +36,7 @@
public static final Interpolator ACCELERATE = new AccelerateInterpolator();
public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
+ public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f);
/**
* Interpolator to be used when animating a move based on a click. Pair with enough duration.
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index 53c2233..9a36aca 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -23,6 +23,9 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.R;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -115,4 +118,8 @@
public <T> T createInstance(Class<T> classType) {
return null;
}
+
+ public AssistManager createAssistManager(BaseStatusBar bar, Context context) {
+ return new AssistManager(bar, context);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java
index 5878219..8bd38db 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java
@@ -95,39 +95,34 @@
private class AssistDisclosureView extends View
implements ValueAnimator.AnimatorUpdateListener {
- public static final int TRACING_ANIMATION_DURATION = 600;
- public static final int ALPHA_IN_ANIMATION_DURATION = 450;
- public static final int ALPHA_OUT_ANIMATION_DURATION = 400;
+ static final int FULL_ALPHA = 222; // 87%
+ static final int ALPHA_IN_ANIMATION_DURATION = 400;
+ static final int ALPHA_OUT_ANIMATION_DURATION = 300;
+
private float mThickness;
private float mShadowThickness;
private final Paint mPaint = new Paint();
private final Paint mShadowPaint = new Paint();
- private final ValueAnimator mTracingAnimator;
private final ValueAnimator mAlphaOutAnimator;
private final ValueAnimator mAlphaInAnimator;
private final AnimatorSet mAnimator;
- private float mTracingProgress = 0;
private int mAlpha = 0;
public AssistDisclosureView(Context context) {
super(context);
- mTracingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(TRACING_ANIMATION_DURATION);
- mTracingAnimator.addUpdateListener(this);
- mTracingAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext,
- R.interpolator.assist_disclosure_trace));
- mAlphaInAnimator = ValueAnimator.ofInt(0, 255).setDuration(ALPHA_IN_ANIMATION_DURATION);
+ mAlphaInAnimator = ValueAnimator.ofInt(0, FULL_ALPHA)
+ .setDuration(ALPHA_IN_ANIMATION_DURATION);
mAlphaInAnimator.addUpdateListener(this);
- mAlphaInAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mAlphaOutAnimator = ValueAnimator.ofInt(255, 0).setDuration(
+ mAlphaInAnimator.setInterpolator(Interpolators.CUSTOM_40_40);
+ mAlphaOutAnimator = ValueAnimator.ofInt(FULL_ALPHA, 0).setDuration(
ALPHA_OUT_ANIMATION_DURATION);
mAlphaOutAnimator.addUpdateListener(this);
- mAlphaOutAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ mAlphaOutAnimator.setInterpolator(Interpolators.CUSTOM_40_40);
mAnimator = new AnimatorSet();
- mAnimator.play(mAlphaInAnimator).with(mTracingAnimator);
mAnimator.play(mAlphaInAnimator).before(mAlphaOutAnimator);
mAnimator.addListener(new AnimatorListenerAdapter() {
boolean mCancelled;
@@ -174,8 +169,6 @@
super.onDetachedFromWindow();
mAnimator.cancel();
-
- mTracingProgress = 0;
mAlpha = 0;
}
@@ -197,45 +190,32 @@
final int width = getWidth();
final int height = getHeight();
float thickness = mThickness;
- final float pixelProgress = mTracingProgress * (width + height - 2 * thickness);
- float bottomProgress = Math.min(pixelProgress, width / 2f);
- if (bottomProgress > 0) {
- drawBeam(canvas,
- width / 2f - bottomProgress,
- height - thickness,
- width / 2f + bottomProgress,
- height, paint, padding);
- }
+ // bottom
+ drawBeam(canvas,
+ 0,
+ height - thickness,
+ width,
+ height, paint, padding);
- float sideProgress = Math.min(pixelProgress - bottomProgress, height - thickness);
- if (sideProgress > 0) {
- drawBeam(canvas,
- 0,
- (height - thickness) - sideProgress,
- thickness,
- height - thickness, paint, padding);
- drawBeam(canvas,
- width - thickness,
- (height - thickness) - sideProgress,
- width,
- height - thickness, paint, padding);
- }
+ // sides
+ drawBeam(canvas,
+ 0,
+ 0,
+ thickness,
+ height - thickness, paint, padding);
+ drawBeam(canvas,
+ width - thickness,
+ 0,
+ width,
+ height - thickness, paint, padding);
- float topProgress = Math.min(pixelProgress - bottomProgress - sideProgress,
- width / 2 - thickness);
- if (sideProgress > 0 && topProgress > 0) {
- drawBeam(canvas,
- thickness,
- 0,
- thickness + topProgress,
- thickness, paint, padding);
- drawBeam(canvas,
- (width - thickness) - topProgress,
- 0,
- width - thickness,
- thickness, paint, padding);
- }
+ // top
+ drawBeam(canvas,
+ thickness,
+ 0,
+ width - thickness,
+ thickness, paint, padding);
}
private void drawBeam(Canvas canvas, float left, float top, float right, float bottom,
@@ -253,8 +233,6 @@
mAlpha = (int) mAlphaOutAnimator.getAnimatedValue();
} else if (animation == mAlphaInAnimator) {
mAlpha = (int) mAlphaInAnimator.getAnimatedValue();
- } else if (animation == mTracingAnimator) {
- mTracingProgress = (float) mTracingAnimator.getAnimatedValue();
}
invalidate();
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index a0aeb2f..a5e771f 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -30,6 +30,7 @@
import com.android.internal.app.AssistUtils;
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.systemui.R;
+import com.android.systemui.SystemUIFactory;
import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.CommandQueue;
@@ -103,6 +104,10 @@
}
}
+ protected boolean shouldShowOrb() {
+ return true;
+ }
+
public void startAssist(Bundle args) {
final ComponentName assistComponent = getAssistInfo();
if (assistComponent == null) {
@@ -110,7 +115,7 @@
}
final boolean isService = assistComponent.equals(getVoiceInteractorComponentName());
- if (!isService || !isVoiceSessionRunning()) {
+ if (!isService || (!isVoiceSessionRunning() && shouldShowOrb())) {
showOrb(assistComponent, isService);
mView.postDelayed(mHideRunnable, isService
? TIMEOUT_SERVICE
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 3370091..5f1b042 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -39,6 +39,8 @@
import android.util.Log;
import android.view.Display;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
@@ -538,6 +540,10 @@
mWakeLock.acquire();
try {
if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
+ if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
+ int subType = (int) event.values[0];
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_AMBIENT_GESTURE, subType);
+ }
if (mDebugVibrate) {
final Vibrator v = (Vibrator) mContext.getSystemService(
Context.VIBRATOR_SERVICE);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index cad7f64..3bbcf6e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -360,7 +360,7 @@
mSwitchingUser = false;
if (userId != UserHandle.USER_SYSTEM) {
UserInfo info = UserManager.get(mContext).getUserInfo(userId);
- if (info != null && info.isGuest()) {
+ if (info != null && (info.isGuest() || info.isDemo())) {
// If we just switched to a guest, try to dismiss keyguard.
dismiss();
}
@@ -399,8 +399,7 @@
sendUserPresentBroadcast();
synchronized (KeyguardViewMediator.this) {
// If system user is provisioned, we might want to lock now to avoid showing launcher
- if (UserManager.isSplitSystemUser()
- && KeyguardUpdateMonitor.getCurrentUser() == UserHandle.USER_SYSTEM) {
+ if (mustNotUnlockCurrentUser()) {
doKeyguardLocked(null);
}
}
@@ -599,7 +598,6 @@
return KeyguardSecurityView.PROMPT_REASON_WRONG_CREDENTIAL;
}
-
return KeyguardSecurityView.PROMPT_REASON_NONE;
}
};
@@ -608,6 +606,11 @@
mPM.userActivity(SystemClock.uptimeMillis(), false);
}
+ boolean mustNotUnlockCurrentUser() {
+ return (UserManager.isSplitSystemUser() || UserManager.isDeviceInDemoMode(mContext))
+ && KeyguardUpdateMonitor.getCurrentUser() == UserHandle.USER_SYSTEM;
+ }
+
private void setupLocked() {
mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mWM = WindowManagerGlobal.getWindowManagerService();
@@ -1176,8 +1179,7 @@
}
// In split system user mode, we never unlock system user.
- if (!UserManager.isSplitSystemUser()
- || KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM
+ if (!mustNotUnlockCurrentUser()
|| !mUpdateMonitor.isDeviceProvisioned()) {
// if the setup wizard hasn't run yet, don't show
@@ -1615,8 +1617,7 @@
synchronized (KeyguardViewMediator.this) {
if (DEBUG) Log.d(TAG, "handleHide");
- if (UserManager.isSplitSystemUser()
- && KeyguardUpdateMonitor.getCurrentUser() == UserHandle.USER_SYSTEM) {
+ if (mustNotUnlockCurrentUser()) {
// In split system user mode, we never unlock system user. The end user has to
// switch to another user.
// TODO: We should stop it early by disabling the swipe up flow. Right now swipe up
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
index 932b4f5..65d6805 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -57,6 +58,7 @@
private boolean mIsVisible;
private boolean mIsIconVisible;
private int mFooterTextId;
+ private int mFooterIconId;
public QSFooter(QSPanel qsPanel, Context context) {
mRootView = LayoutInflater.from(context)
@@ -64,6 +66,7 @@
mRootView.setOnClickListener(this);
mFooterText = (TextView) mRootView.findViewById(R.id.footer_text);
mFooterIcon = (ImageView) mRootView.findViewById(R.id.footer_icon);
+ mFooterIconId = R.drawable.ic_qs_vpn;
mContext = context;
mMainHandler = new Handler();
}
@@ -118,6 +121,14 @@
mIsVisible = true;
} else {
mFooterTextId = R.string.vpn_footer;
+ // Update the VPN footer icon, if needed.
+ int footerIconId = (mSecurityController.isVpnBranded()
+ ? R.drawable.ic_qs_branded_vpn
+ : R.drawable.ic_qs_vpn);
+ if (mFooterIconId != footerIconId) {
+ mFooterIcon.setImageResource(footerIconId);
+ mFooterIconId = footerIconId;
+ }
mIsVisible = mIsIconVisible;
}
mMainHandler.post(mUpdateDisplayState);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 636a9a50..ce6aa71 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -106,8 +106,6 @@
R.layout.qs_paged_tile_layout, this, false);
mTileLayout.setListening(mListening);
addView((View) mTileLayout);
- findViewById(android.R.id.edit).setOnClickListener(view ->
- mHost.startRunnableDismissingKeyguard(() -> showEdit(view)));
}
public boolean isShowingCustomize() {
@@ -369,7 +367,7 @@
}
- private void showEdit(final View v) {
+ public void showEdit(final View v) {
v.post(new Runnable() {
@Override
public void run() {
@@ -377,8 +375,8 @@
if (!mCustomizePanel.isCustomizing()) {
int[] loc = new int[2];
v.getLocationInWindow(loc);
- int x = loc[0];
- int y = loc[1];
+ int x = loc[0] + v.getWidth() / 2;
+ int y = loc[1] + v.getHeight() / 2;
mCustomizePanel.show(x, y);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index f287f1b..f09275d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -146,12 +146,11 @@
};
public int getNumQuickTiles(Context context) {
- return TunerService.get(context).getValue(NUM_QUICK_TILES, 5);
+ return TunerService.get(context).getValue(NUM_QUICK_TILES, 6);
}
private static class HeaderTileLayout extends LinearLayout implements QSTileLayout {
- private final Space mEndSpacer;
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
private boolean mListening;
@@ -161,25 +160,6 @@
setClipToPadding(false);
setGravity(Gravity.CENTER_VERTICAL);
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-
- mEndSpacer = new Space(context);
- mEndSpacer.setLayoutParams(generateLayoutParams());
- updateDownArrowMargin();
- addView(mEndSpacer);
- setOrientation(LinearLayout.HORIZONTAL);
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- updateDownArrowMargin();
- }
-
- private void updateDownArrowMargin() {
- LayoutParams params = (LayoutParams) mEndSpacer.getLayoutParams();
- params.setMarginStart(mContext.getResources().getDimensionPixelSize(
- R.dimen.qs_expand_margin));
- mEndSpacer.setLayoutParams(params);
}
@Override
@@ -193,11 +173,11 @@
@Override
public void addTile(TileRecord tile) {
- addView(tile.tileView, getChildCount() - 1 /* Leave icon at end */,
- generateLayoutParams());
- // Add a spacer.
- addView(new Space(mContext), getChildCount() - 1 /* Leave icon at end */,
- generateSpaceParams());
+ if (getChildCount() != 0) {
+ // Add a spacer.
+ addView(new Space(mContext), getChildCount(), generateSpaceParams());
+ }
+ addView(tile.tileView, getChildCount(), generateLayoutParams());
mRecords.add(tile);
tile.tile.setListening(this, mListening);
}
@@ -222,8 +202,10 @@
int childIndex = getChildIndex(tile.tileView);
// Remove the tile.
removeViewAt(childIndex);
- // Remove its spacer as well.
- removeViewAt(childIndex);
+ if (getChildCount() != 0) {
+ // Remove its spacer as well.
+ removeViewAt(childIndex);
+ }
mRecords.remove(tile);
tile.tile.setListening(this, false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 777ed6a..3d192e5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -57,8 +57,7 @@
}
private void addSystemTiles(final QSTileHost host) {
- String possible = mContext.getString(R.string.quick_settings_tiles_default)
- + ",hotspot,inversion,saver,work,cast,night";
+ String possible = mContext.getString(R.string.quick_settings_tiles_stock);
String[] possibleTiles = possible.split(",");
final Handler qsHandler = new Handler(host.getLooper());
final Handler mainHandler = new Handler(Looper.getMainLooper());
@@ -145,9 +144,16 @@
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
+ String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
for (ResolveInfo info : services) {
String packageName = info.serviceInfo.packageName;
ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
+
+ // Don't include apps that are a part of the default tile set.
+ if (stockTiles.contains(componentName.flattenToString())) {
+ continue;
+ }
+
final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
String spec = CustomTile.toSpec(componentName);
State state = getState(params[0], spec);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 79e06c6..9211562 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -72,7 +72,6 @@
private static final String TAG = KeyboardShortcuts.class.getSimpleName();
private static final Object sLock = new Object();
private static KeyboardShortcuts sInstance;
- private static boolean sIsShowing;
private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
private final SparseArray<String> mModifierNames = new SparseArray<>();
@@ -131,13 +130,12 @@
dismiss();
}
getInstance(context).showKeyboardShortcuts(deviceId);
- sIsShowing = true;
}
}
public static void toggle(Context context, int deviceId) {
synchronized (sLock) {
- if (sIsShowing) {
+ if (isShowing()) {
dismiss();
} else {
show(context, deviceId);
@@ -151,10 +149,14 @@
sInstance.dismissKeyboardShortcuts();
sInstance = null;
}
- sIsShowing = false;
}
}
+ private static boolean isShowing() {
+ return sInstance != null && sInstance.mKeyboardShortcutsDialog != null
+ && sInstance.mKeyboardShortcutsDialog.isShowing();
+ }
+
private void loadResources(Context context) {
mSpecialCharacterNames.put(
KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
index dba7130..bfa43fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
@@ -42,6 +42,7 @@
private float mViewAlpha = 1.0f;
private ValueAnimator mAlphaAnimator;
private Rect mExcludedRect = new Rect();
+ private int mLeftInset = 0;
private boolean mHasExcludedArea;
private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener
= new ValueAnimator.AnimatorUpdateListener() {
@@ -87,12 +88,12 @@
if (mExcludedRect.top > 0) {
canvas.drawRect(0, 0, getWidth(), mExcludedRect.top, mPaint);
}
- if (mExcludedRect.left > 0) {
- canvas.drawRect(0, mExcludedRect.top, mExcludedRect.left, mExcludedRect.bottom,
- mPaint);
+ if (mExcludedRect.left + mLeftInset > 0) {
+ canvas.drawRect(0, mExcludedRect.top, mExcludedRect.left + mLeftInset,
+ mExcludedRect.bottom, mPaint);
}
- if (mExcludedRect.right < getWidth()) {
- canvas.drawRect(mExcludedRect.right,
+ if (mExcludedRect.right + mLeftInset < getWidth()) {
+ canvas.drawRect(mExcludedRect.right + mLeftInset,
mExcludedRect.top,
getWidth(),
mExcludedRect.bottom,
@@ -183,4 +184,14 @@
public void setChangeRunnable(Runnable changeRunnable) {
mChangeRunnable = changeRunnable;
}
+
+ public void setLeftInset(int leftInset) {
+ if (mLeftInset != leftInset) {
+ mLeftInset = leftInset;
+
+ if (mHasExcludedArea) {
+ invalidate();
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
index 599e575a..74caa53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
@@ -67,6 +67,8 @@
private boolean mNoSimsVisible = false;
private boolean mVpnVisible = false;
+ private int mVpnIconId = 0;
+ private int mLastVpnIconId = -1;
private boolean mEthernetVisible = false;
private int mEthernetIconId = 0;
private int mLastEthernetIconId = -1;
@@ -164,6 +166,7 @@
mSC = sc;
mSC.addCallback(this);
mVpnVisible = mSC.isVpnEnabled();
+ mVpnIconId = currentVpnIconId(mSC.isVpnBranded());
}
@Override
@@ -249,6 +252,7 @@
@Override
public void run() {
mVpnVisible = mSC.isVpnEnabled();
+ mVpnIconId = currentVpnIconId(mSC.isVpnBranded());
apply();
}
});
@@ -436,6 +440,15 @@
if (mWifiGroup == null) return;
mVpn.setVisibility(mVpnVisible ? View.VISIBLE : View.GONE);
+ if (mVpnVisible) {
+ if (mLastVpnIconId != mVpnIconId) {
+ setIconForView(mVpn, mVpnIconId);
+ mLastVpnIconId = mVpnIconId;
+ }
+ mVpn.setVisibility(View.VISIBLE);
+ } else {
+ mVpn.setVisibility(View.GONE);
+ }
if (DEBUG) Log.d(TAG, String.format("vpn: %s", mVpnVisible ? "VISIBLE" : "GONE"));
if (mEthernetVisible) {
@@ -564,6 +577,10 @@
v.setImageTintList(ColorStateList.valueOf(tint));
}
+ private int currentVpnIconId(boolean isBranded) {
+ return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
+ }
+
private class PhoneState {
private final int mSubId;
private boolean mMobileVisible = false;
@@ -685,4 +702,3 @@
}
}
}
-
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index 30d24ff..b5a48a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -14,11 +14,10 @@
package com.android.systemui.statusbar.phone;
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
import android.graphics.drawable.Drawable;
import android.view.View;
-import android.widget.ImageView;
-
-import com.android.systemui.statusbar.policy.KeyButtonView;
import java.util.ArrayList;
@@ -65,9 +64,9 @@
view.setVisibility(mVisibility);
}
if (mImageResource > 0) {
- ((ImageView) view).setImageResource(mImageResource);
+ ((ButtonInterface) view).setImageResource(mImageResource);
} else if (mImageDrawable != null) {
- ((ImageView) view).setImageDrawable(mImageDrawable);
+ ((ButtonInterface) view).setImageDrawable(mImageDrawable);
}
}
@@ -88,7 +87,7 @@
mImageResource = -1;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
- ((ImageView) mViews.get(i)).setImageDrawable(mImageDrawable);
+ ((ButtonInterface) mViews.get(i)).setImageDrawable(mImageDrawable);
}
}
@@ -97,7 +96,7 @@
mImageDrawable = null;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
- ((ImageView) mViews.get(i)).setImageResource(mImageResource);
+ ((ButtonInterface) mViews.get(i)).setImageResource(mImageResource);
}
}
@@ -114,7 +113,7 @@
// This seems to be an instantaneous thing, so not going to persist it.
final int N = mViews.size();
for (int i = 0; i < N; i++) {
- ((KeyButtonView) mViews.get(i)).abortCurrentGesture();
+ ((ButtonInterface) mViews.get(i)).abortCurrentGesture();
}
}
@@ -165,4 +164,15 @@
public void setCurrentView(View currentView) {
mCurrentView = currentView.findViewById(mId);
}
+
+ /**
+ * Interface for ImageView button actions.
+ */
+ public interface ButtonInterface {
+ void setImageResource(@DrawableRes int resId);
+
+ void setImageDrawable(@Nullable Drawable drawable);
+
+ void abortCurrentGesture();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 2bee816..dd46b08 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -26,7 +26,9 @@
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Space;
+
import com.android.systemui.R;
+import com.android.systemui.SystemUIFactory;
import com.android.systemui.statusbar.policy.KeyButtonView;
import com.android.systemui.tuner.TunerService;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 5fab796..53fe6ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -380,10 +380,6 @@
&& ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0);
final boolean disableSearch = ((disabledFlags & View.STATUS_BAR_DISABLE_SEARCH) != 0);
- if (SLIPPERY_WHEN_DISABLED) {
- setSlippery(disableHome && disableRecent && disableBack && disableSearch);
- }
-
ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
if (navButtons != null) {
LayoutTransition lt = navButtons.getLayoutTransition();
@@ -458,22 +454,6 @@
}
}
- public void setSlippery(boolean newSlippery) {
- WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
- if (lp != null) {
- boolean oldSlippery = (lp.flags & WindowManager.LayoutParams.FLAG_SLIPPERY) != 0;
- if (!oldSlippery && newSlippery) {
- lp.flags |= WindowManager.LayoutParams.FLAG_SLIPPERY;
- } else if (oldSlippery && !newSlippery) {
- lp.flags &= ~WindowManager.LayoutParams.FLAG_SLIPPERY;
- } else {
- return;
- }
- WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
- wm.updateViewLayout(this, lp);
- }
- }
-
public void setMenuVisibility(final boolean show) {
setMenuVisibility(show, false);
}
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 ca42ba0f..fde3626 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -765,7 +765,7 @@
// no window manager? good luck with that
}
- mAssistManager = new AssistManager(this, context);
+ mAssistManager = SystemUIFactory.getInstance().createAssistManager(this, context);
// figure out which pixel-format to use for the status bar.
mPixelFormat = PixelFormat.OPAQUE;
@@ -1372,7 +1372,8 @@
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_SLIPPERY,
PixelFormat.TRANSLUCENT);
// this will allow the navbar to run in an overlay on devices that support this
if (ActivityManager.isHighEndGfx()) {
@@ -2578,8 +2579,6 @@
}
mExpandedVisible = true;
- if (mNavigationBarView != null)
- mNavigationBarView.setSlippery(true);
// Expand the window to encompass the full screen in anticipation of the drag.
// This is only possible to do atomically because the status bar is at the top of the screen!
@@ -2713,8 +2712,7 @@
mNotificationPanel.closeQs();
mExpandedVisible = false;
- if (mNavigationBarView != null)
- mNavigationBarView.setSlippery(false);
+
visibilityChanged(false);
// Shrink the window to the size of the status bar only
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 1fd3bdf..0563f4a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -221,6 +221,8 @@
mSimState = IccCardConstants.State.ABSENT;
} else if (IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR.equals(stateExtra)) {
mSimState = IccCardConstants.State.CARD_IO_ERROR;
+ } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED.equals(stateExtra)) {
+ mSimState = IccCardConstants.State.CARD_RESTRICTED;
} else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(stateExtra)) {
mSimState = IccCardConstants.State.READY;
} else if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(stateExtra)) {
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 fa57775..011ec22 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
@@ -27,6 +27,7 @@
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.quicksettings.Tile;
@@ -321,6 +322,9 @@
return;
}
if (DEBUG) Log.d(TAG, "Recreating tiles");
+ if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
+ newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
+ }
final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
int currentUser = ActivityManager.getCurrentUser();
if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
index e091d6dc..40a93df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
@@ -23,6 +23,7 @@
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
+import android.os.UserManager;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
@@ -82,18 +83,16 @@
protected MultiUserSwitch mMultiUserSwitch;
private ImageView mMultiUserAvatar;
- private float mDateTimeTranslation;
- private float mDateTimeAlarmTranslation;
private float mDateScaleFactor;
protected float mGearTranslation;
private TouchAnimator mSecondHalfAnimator;
private TouchAnimator mFirstHalfAnimator;
private TouchAnimator mDateSizeAnimator;
- private TouchAnimator mAlarmTranslation;
protected TouchAnimator mSettingsAlpha;
private float mExpansionAmount;
private QSTileHost mHost;
+ private View mEdit;
private boolean mShowFullAlarm;
public QuickStatusBarHeader(Context context, AttributeSet attrs) {
@@ -106,6 +105,10 @@
mEmergencyOnly = (TextView) findViewById(R.id.header_emergency_calls_only);
+ mEdit = findViewById(android.R.id.edit);
+ findViewById(android.R.id.edit).setOnClickListener(view ->
+ mHost.startRunnableDismissingKeyguard(() -> mQsPanel.showEdit(view)));
+
mDateTimeAlarmGroup = (ViewGroup) findViewById(R.id.date_time_alarm_group);
mDateTimeAlarmGroup.findViewById(R.id.empty_time_view).setVisibility(View.GONE);
mDateTimeGroup = (ViewGroup) findViewById(R.id.date_time_group);
@@ -154,16 +157,11 @@
mGearTranslation = mContext.getResources().getDimension(R.dimen.qs_header_gear_translation);
- mDateTimeTranslation = mContext.getResources().getDimension(
- R.dimen.qs_date_anim_translation);
- mDateTimeAlarmTranslation = mContext.getResources().getDimension(
- R.dimen.qs_date_alarm_anim_translation);
float dateCollapsedSize = mContext.getResources().getDimension(
R.dimen.qs_date_collapsed_text_size);
float dateExpandedSize = mContext.getResources().getDimension(
R.dimen.qs_date_text_size);
mDateScaleFactor = dateExpandedSize / dateCollapsedSize;
- updateDateTimePosition();
mSecondHalfAnimator = new TouchAnimator.Builder()
.addFloat(mShowFullAlarm ? mAlarmStatus : findViewById(R.id.date), "alpha", 0, 1)
@@ -187,10 +185,9 @@
protected void updateSettingsAnimator() {
mSettingsAlpha = new TouchAnimator.Builder()
- .addFloat(mSettingsContainer, "translationY", -mGearTranslation, 0)
+ .addFloat(mEdit, "translationY", -mGearTranslation, 0)
.addFloat(mMultiUserSwitch, "translationY", -mGearTranslation, 0)
- .addFloat(mSettingsButton, "rotation", -90, 0)
- .addFloat(mSettingsContainer, "alpha", 0, 1)
+ .addFloat(mEdit, "alpha", 0, 1)
.addFloat(mMultiUserSwitch, "alpha", 0, 1)
.setStartDelay(QSAnimator.EXPANDED_TILE_DELAY)
.build();
@@ -252,7 +249,6 @@
mFirstHalfAnimator.setPosition(headerExpansionFraction);
}
mDateSizeAnimator.setPosition(headerExpansionFraction);
- mAlarmTranslation.setPosition(headerExpansionFraction);
mSettingsAlpha.setPosition(headerExpansionFraction);
updateAlarmVisibilities();
@@ -273,15 +269,6 @@
mAlarmStatusCollapsed.setVisibility(mAlarmShowing ? View.VISIBLE : View.INVISIBLE);
}
- private void updateDateTimePosition() {
- // This one has its own because we have to rebuild it every time the alarm state changes.
- mAlarmTranslation = new TouchAnimator.Builder()
- .addFloat(mDateTimeAlarmGroup, "translationY", 0, mAlarmShowing
- ? mDateTimeAlarmTranslation : mDateTimeTranslation)
- .build();
- mAlarmTranslation.setPosition(mExpansionAmount);
- }
-
public void setListening(boolean listening) {
if (listening == mListening) {
return;
@@ -293,7 +280,6 @@
@Override
public void updateEverything() {
- updateDateTimePosition();
updateVisibilities();
setClickable(false);
}
@@ -302,11 +288,12 @@
updateAlarmVisibilities();
mEmergencyOnly.setVisibility(mExpanded && mShowEmergencyCallsOnly
? View.VISIBLE : View.INVISIBLE);
- mSettingsContainer.setVisibility(mExpanded ? View.VISIBLE : View.INVISIBLE);
mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility(
TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE);
- mMultiUserSwitch.setVisibility(mExpanded && mMultiUserSwitch.hasMultipleUsers()
+ final boolean isDemo = UserManager.isDeviceInDemoMode(mContext);
+ mMultiUserSwitch.setVisibility(mExpanded && mMultiUserSwitch.hasMultipleUsers() && !isDemo
? View.VISIBLE : View.INVISIBLE);
+ mEdit.setVisibility(isDemo ? View.INVISIBLE : View.VISIBLE);
}
private void updateListeners() {
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 9c4480e..135c294 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -524,6 +524,10 @@
mScrimBehind.setExcludedArea(area);
}
+ public void setLeftInset(int inset) {
+ mScrimBehind.setLeftInset(inset);
+ }
+
public int getScrimBehindColor() {
return mScrimBehind.getScrimColorWithAlpha();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index ebfa018..7b22b88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -70,6 +70,7 @@
private View mBrightnessMirror;
private int mRightInset = 0;
+ private int mLeftInset = 0;
private PhoneStatusBar mService;
private final Paint mTransparentSrcPaint = new Paint();
@@ -93,25 +94,26 @@
@Override
protected boolean fitSystemWindows(Rect insets) {
if (getFitsSystemWindows()) {
- boolean paddingChanged = insets.left != getPaddingLeft()
- || insets.top != getPaddingTop()
+ boolean paddingChanged = insets.top != getPaddingTop()
|| insets.bottom != getPaddingBottom();
// Super-special right inset handling, because scrims and backdrop need to ignore it.
- if (insets.right != mRightInset) {
+ if (insets.right != mRightInset || insets.left != mLeftInset) {
mRightInset = insets.right;
+ mLeftInset = insets.left;
applyMargins();
}
- // Drop top inset, apply left inset and pass through bottom inset.
+ // Drop top inset, and pass through bottom inset.
if (paddingChanged) {
- setPadding(insets.left, 0, 0, 0);
+ setPadding(0, 0, 0, 0);
}
insets.left = 0;
insets.top = 0;
insets.right = 0;
} else {
- if (mRightInset != 0) {
+ if (mRightInset != 0 || mLeftInset != 0) {
mRightInset = 0;
+ mLeftInset = 0;
applyMargins();
}
boolean changed = getPaddingLeft() != 0
@@ -127,13 +129,16 @@
}
private void applyMargins() {
+ mService.mScrimController.setLeftInset(mLeftInset);
final int N = getChildCount();
for (int i = 0; i < N; i++) {
View child = getChildAt(i);
if (child.getLayoutParams() instanceof LayoutParams) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (!lp.ignoreRightInset && lp.rightMargin != mRightInset) {
+ if (!lp.ignoreRightInset
+ && (lp.rightMargin != mRightInset || lp.leftMargin != mLeftInset)) {
lp.rightMargin = mRightInset;
+ lp.leftMargin = mLeftInset;
child.requestLayout();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
index c8c824a..d8b1a62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.policy;
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
@@ -42,11 +44,12 @@
import android.widget.ImageView;
import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.ButtonDispatcher;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
-public class KeyButtonView extends ImageView {
+public class KeyButtonView extends ImageView implements ButtonDispatcher.ButtonInterface {
private int mContentDescriptionRes;
private long mDownTime;
@@ -247,10 +250,21 @@
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
+ @Override
public void abortCurrentGesture() {
setPressed(false);
mGestureAborted = true;
}
+
+ @Override
+ public void setImageResource(@DrawableRes int resId) {
+ super.setImageResource(resId);
+ }
+
+ @Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ super.setImageDrawable(drawable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index a633241..b27bfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -810,7 +810,7 @@
private SubscriptionInfo addSignalController(int id, int simSlotIndex) {
SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0,
- null, 0, 0, "", SubscriptionManager.SIM_PROVISIONED);
+ null, 0, 0, "");
mMobileSignalControllers.put(id, new MobileSignalController(mContext,
mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info,
mSubDefaults, mReceiverHandler.getLooper()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 7704a07..a2289c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -21,6 +21,7 @@
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ShortcutManager;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -132,6 +133,15 @@
mEditText.mShowImeOnInputConnection = false;
mController.remoteInputSent(mEntry);
+ // Tell ShortcutManager that this package has been "activated". ShortcutManager
+ // will reset the throttling for this package.
+ // Strictly speaking, the intent receiver may be different from the notification publisher,
+ // but that's an edge case, and also because we can't always know which package will receive
+ // an intent, so we just reset for the publisher.
+ getContext().getSystemService(ShortcutManager.class).onApplicationActive(
+ mEntry.notification.getPackageName(),
+ mEntry.notification.getUser().getIdentifier());
+
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
mEntry.notification.getPackageName());
try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index a22f988..014afae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -23,6 +23,8 @@
String getProfileOwnerName();
boolean isVpnEnabled();
boolean isVpnRestricted();
+ /** Whether the VPN app should use branded VPN iconography. */
+ boolean isVpnBranded();
String getPrimaryVpnName();
String getProfileVpnName();
void onUserSwitched(int newUserId);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 5046456..07d3b59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -18,6 +18,8 @@
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.net.ConnectivityManager;
@@ -54,10 +56,13 @@
.build();
private static final int NO_NETWORK = -1;
+ private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED";
+
private final Context mContext;
private final ConnectivityManager mConnectivityManager;
private final IConnectivityManager mConnectivityManagerService;
private final DevicePolicyManager mDevicePolicyManager;
+ private final PackageManager mPackageManager;
private final UserManager mUserManager;
@GuardedBy("mCallbacks")
@@ -75,6 +80,7 @@
context.getSystemService(Context.CONNECTIVITY_SERVICE);
mConnectivityManagerService = IConnectivityManager.Stub.asInterface(
ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+ mPackageManager = context.getPackageManager();
mUserManager = (UserManager)
context.getSystemService(Context.USER_SERVICE);
@@ -165,6 +171,21 @@
}
@Override
+ public boolean isVpnBranded() {
+ VpnConfig cfg = mCurrentVpns.get(mVpnUserId);
+ if (cfg == null) {
+ return false;
+ }
+
+ String packageName = getPackageNameForVpnConfig(cfg);
+ if (packageName == null) {
+ return false;
+ }
+
+ return isVpnPackageBranded(packageName);
+ }
+
+ @Override
public void removeCallback(SecurityControllerCallback callback) {
synchronized (mCallbacks) {
if (callback == null) return;
@@ -245,6 +266,28 @@
mCurrentVpns = vpns;
}
+ private String getPackageNameForVpnConfig(VpnConfig cfg) {
+ if (cfg.legacy) {
+ return null;
+ }
+ return cfg.user;
+ }
+
+ private boolean isVpnPackageBranded(String packageName) {
+ boolean isBranded;
+ try {
+ ApplicationInfo info = mPackageManager.getApplicationInfo(packageName,
+ PackageManager.GET_META_DATA);
+ if (info == null || info.metaData == null || !info.isSystemApp()) {
+ return false;
+ }
+ isBranded = info.metaData.getBoolean(VPN_BRANDED_META_DATA, false);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ return isBranded;
+ }
+
private final NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onAvailable(Network network) {
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index a1487e3..4ed0913 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2212,12 +2212,132 @@
// Notification group expansion state toggled by the expand affordance.
ACTION_NOTIFICATION_GROUP_EXPANDER = 408;
+
// Notification expansion state toggled by the expand gesture.
ACTION_NOTIFICATION_GESTURE_EXPANDER = 409;
// Notification group expansion state toggled by the expand gesture.
ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER = 410;
+ // User performs gesture that activates the ambient display
+ // 1: Gesture performed is Nudge
+ // 2: Gesture performed is Pickup
+ // 4: Gesture performed is Double Tap
+ ACTION_AMBIENT_GESTURE = 411;
+
+ // ---- End N Constants, all N constants go above this line ----
+
+ // ------- Begin N App Disambig Shade -----
+ // Application disambig shade opened or closed with a featured app.
+ // These are actually visibility events, but visible/hidden doesn't
+ // take a package, so these are being logged as actions.
+ // Package: Calling app on open, called app on close
+ ACTION_SHOW_APP_DISAMBIG_APP_FEATURED = 451;
+ ACTION_HIDE_APP_DISAMBIG_APP_FEATURED = 452;
+
+ // Application disambig shade opened or closed without a featured app.
+ // These are actually visibility events, but visible/hidden doesn't
+ // take a package, so these are being logged as actions.
+ // Package: Calling app on open, called app on close
+ ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED = 453;
+ ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED = 454;
+
+ // User opens in an app by pressing “Always” in the application disambig shade.
+ // Subtype: Index of selection
+ ACTION_APP_DISAMBIG_ALWAYS = 455;
+
+ // User opens in an app by pressing “Just Once” in the application disambig shade.
+ // Subtype: Index of selection
+ ACTION_APP_DISAMBIG_JUST_ONCE = 456;
+
+ // User opens in an app by tapping on its name in the application disambig shade.
+ // Subtype: Index of selection
+ ACTION_APP_DISAMBIG_TAP = 457;
+
+ // OPEN: Settings > Internal storage > Storage manager
+ // CATEGORY: SETTINGS
+ STORAGE_MANAGER_SETTINGS = 458;
+
+ // OPEN: Settings -> Gestures
+ // CATEGORY: SETTINGS
+ SETTINGS_GESTURES = 459;
+
+ // ------ Begin Deletion Helper ------
+ // ACTION: Settings > Storage > Free Up Space > Photos & Videos > Toggle
+ // SUBTYPE: false is off, true is on
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_SELECTION_PHOTOS = 460;
+
+ // ACTION: Settings > Storage > Free Up Space > Apps > Toggle
+ // SUBTYPE: false is off, true is on
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_SELECTION_ALL_APPS = 461;
+
+ // ACTION: Settings > Storage > Free Up Space > Apps > Click an unchecked app
+ // CATEGORY: SETTINGS
+ // PACKAGE: Unchecked app
+ ACTION_DELETION_SELECTION_APP_ON = 462;
+
+ // ACTION: Settings > Storage > Free Up Space > Apps > Click a checked app
+ // CATEGORY: SETTINGS
+ // PACKAGE: Checked app
+ ACTION_DELETION_SELECTION_APP_OFF = 463;
+
+ // ACTION: Settings > Storage > Free Up Space > Apps > Click category
+ // SUBTYPE: false is expanded, true is collapsed
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_APPS_COLLAPSED = 464;
+
+ // ACTION: Settings > Storage > Free Up Space > Downloads > Check On
+ // SUBTYPE: false is off, true is on
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_SELECTION_DOWNLOADS = 465;
+
+ // ACTION: Settings > Storage > Free Up Space > Downloads > Click category
+ // SUBTYPE: false is expanded, true is collapsed
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_DOWNLOADS_COLLAPSED = 466;
+
+ // ACTION: Settings > Storage > Free Up Space > Free up ... GB
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_HELPER_CLEAR = 467;
+
+ // ACTION: Settings > Storage > Free Up Space > Cancel
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_HELPER_CANCEL = 468;
+
+ // ACTION: Settings > Storage > Free Up Space > Free up ... GB > Remove
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_HELPER_REMOVE_CONFIRM = 469;
+
+ // ACTION: Settings > Storage > Free Up Space > Free up ... GB > Cancel
+ // CATEGORY: SETTINGS
+ ACTION_DELETION_HELPER_REMOVE_CANCEL = 470;
+
+ // Deletion helper encountered an error during package deletion.
+ ACTION_DELETION_HELPER_APPS_DELETION_FAIL = 471;
+
+ // Deletion helper encountered an error during downloads folder deletion.
+ ACTION_DELETION_HELPER_DOWNLOADS_DELETION_FAIL = 472;
+
+ // Deletion helper encountered an error during photo and video deletion.
+ ACTION_DELETION_HELPER_PHOTOS_VIDEOS_DELETION_FAIL = 473;
+
+ // OPEN: Settings (root page if there are multiple tabs)
+ // CATEGORY: SETTINGS
+ DASHBOARD_CONTAINER = 474;
+
+ // OPEN: Settings -> SUPPORT TAB
+ // CATEGORY: SETTINGS
+ SUPPORT_FRAGMENT = 475;
+
+ // ACTION: Settings -> Select summary tab.
+ ACTION_SELECT_SUMMARY=476;
+
+ // ACTION: Settings -> Select support tab.
+ ACTION_SELECT_SUPPORT_FRAGMENT = 477;
+
+ // ---- End N-MR1 Constants, all N-MR1 constants go above this line ----
// Add new aosp constants above this line.
// END OF AOSP CONSTANTS
}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 2a30229..feceb14 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -30,6 +30,7 @@
import android.app.admin.DevicePolicyManagerInternal.OnCrossProfileWidgetProvidersChangeListener;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
+import android.appwidget.PendingHostUpdate;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -72,10 +73,12 @@
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.AttributeSet;
+import android.util.LongSparseArray;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.SparseLongArray;
import android.util.TypedValue;
import android.util.Xml;
import android.view.Display;
@@ -738,8 +741,8 @@
}
@Override
- public ParceledListSlice<RemoteViews> startListening(IAppWidgetHost callbacks,
- String callingPackage, int hostId, int[] appWidgetIds, int[] updatedIds) {
+ public ParceledListSlice<PendingHostUpdate> startListening(IAppWidgetHost callbacks,
+ String callingPackage, int hostId, int[] appWidgetIds) {
final int userId = UserHandle.getCallingUserId();
if (DEBUG) {
@@ -759,18 +762,19 @@
host.callbacks = callbacks;
int N = appWidgetIds.length;
- ArrayList<RemoteViews> outViews = new ArrayList<>(N);
- RemoteViews rv;
- int added = 0;
+ ArrayList<PendingHostUpdate> outUpdates = new ArrayList<>(N);
+
+ LongSparseArray<PendingHostUpdate> updatesMap = new LongSparseArray<>();
for (int i = 0; i < N; i++) {
- rv = host.getPendingViewsForId(appWidgetIds[i]);
- if (rv != null) {
- updatedIds[added] = appWidgetIds[i];
- outViews.add(rv);
- added++;
+ if (host.getPendingUpdatesForId(appWidgetIds[i], updatesMap)) {
+ // We key the updates based on time, so that the values are sorted by time.
+ int M = updatesMap.size();
+ for (int j = 0; j < M; j++) {
+ outUpdates.add(updatesMap.valueAt(j));
+ }
}
}
- return new ParceledListSlice<>(outViews);
+ return new ParceledListSlice<>(outUpdates);
}
}
@@ -1810,6 +1814,15 @@
}
private void scheduleNotifyAppWidgetViewDataChanged(Widget widget, int viewId) {
+ if (viewId == ID_VIEWS_UPDATE || viewId == ID_PROVIDER_CHANGED) {
+ // A view id should never collide with these constants but a developer can call this
+ // method with a wrong id. In that case, ignore the call.
+ return;
+ }
+ long requestTime = SystemClock.uptimeMillis();
+ if (widget != null) {
+ widget.updateTimes.put(viewId, requestTime);
+ }
if (widget == null || widget.host == null || widget.host.zombie
|| widget.host.callbacks == null || widget.provider == null
|| widget.provider.zombie) {
@@ -1819,6 +1832,7 @@
SomeArgs args = SomeArgs.obtain();
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
+ args.arg3 = requestTime;
args.argi1 = widget.appWidgetId;
args.argi2 = viewId;
@@ -1829,9 +1843,10 @@
private void handleNotifyAppWidgetViewDataChanged(Host host, IAppWidgetHost callbacks,
- int appWidgetId, int viewId) {
+ int appWidgetId, int viewId, long requestTime) {
try {
callbacks.viewDataChanged(appWidgetId, viewId);
+ host.lastWidgetUpdateTime = requestTime;
} catch (RemoteException re) {
// It failed; remove the callback. No need to prune because
// we know that this host is still referenced by this instance.
@@ -1880,7 +1895,7 @@
private void scheduleNotifyUpdateAppWidgetLocked(Widget widget, RemoteViews updateViews) {
long requestTime = SystemClock.uptimeMillis();
if (widget != null) {
- widget.lastUpdateTime = requestTime;
+ widget.updateTimes.put(ID_VIEWS_UPDATE, requestTime);
}
if (widget == null || widget.provider == null || widget.provider.zombie
|| widget.host.callbacks == null || widget.host.zombie) {
@@ -1913,6 +1928,12 @@
}
private void scheduleNotifyProviderChangedLocked(Widget widget) {
+ long requestTime = SystemClock.uptimeMillis();
+ if (widget != null) {
+ // When the provider changes, reset everything else.
+ widget.updateTimes.clear();
+ widget.updateTimes.append(ID_PROVIDER_CHANGED, requestTime);
+ }
if (widget == null || widget.provider == null || widget.provider.zombie
|| widget.host.callbacks == null || widget.host.zombie) {
return;
@@ -1922,6 +1943,7 @@
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
args.arg3 = widget.provider.info;
+ args.arg4 = requestTime;
args.argi1 = widget.appWidgetId;
mCallbackHandler.obtainMessage(
@@ -1930,9 +1952,10 @@
}
private void handleNotifyProviderChanged(Host host, IAppWidgetHost callbacks,
- int appWidgetId, AppWidgetProviderInfo info) {
+ int appWidgetId, AppWidgetProviderInfo info, long requestTime) {
try {
callbacks.providerChanged(appWidgetId, info);
+ host.lastWidgetUpdateTime = requestTime;
} catch (RemoteException re) {
synchronized (mLock){
Slog.e(TAG, "Widget host dead: " + host.id, re);
@@ -3416,10 +3439,11 @@
Host host = (Host) args.arg1;
IAppWidgetHost callbacks = (IAppWidgetHost) args.arg2;
AppWidgetProviderInfo info = (AppWidgetProviderInfo)args.arg3;
+ long requestTime = (Long) args.arg4;
final int appWidgetId = args.argi1;
args.recycle();
- handleNotifyProviderChanged(host, callbacks, appWidgetId, info);
+ handleNotifyProviderChanged(host, callbacks, appWidgetId, info, requestTime);
} break;
case MSG_NOTIFY_PROVIDERS_CHANGED: {
@@ -3435,11 +3459,13 @@
SomeArgs args = (SomeArgs) message.obj;
Host host = (Host) args.arg1;
IAppWidgetHost callbacks = (IAppWidgetHost) args.arg2;
+ long requestTime = (Long) args.arg3;
final int appWidgetId = args.argi1;
final int viewId = args.argi2;
args.recycle();
- handleNotifyAppWidgetViewDataChanged(host, callbacks, appWidgetId, viewId);
+ handleNotifyAppWidgetViewDataChanged(host, callbacks, appWidgetId, viewId,
+ requestTime);
} break;
}
}
@@ -3778,20 +3804,41 @@
}
/**
- * Returns the RemoveViews for the provided widget id if an update is pending
- * for that widget.
+ * Adds all pending updates in {@param outUpdates} keys by the update time.
*/
- public RemoteViews getPendingViewsForId(int appWidgetId) {
+ public boolean getPendingUpdatesForId(int appWidgetId,
+ LongSparseArray<PendingHostUpdate> outUpdates) {
long updateTime = lastWidgetUpdateTime;
int N = widgets.size();
for (int i = 0; i < N; i++) {
Widget widget = widgets.get(i);
- if (widget.appWidgetId == appWidgetId
- && widget.lastUpdateTime > updateTime) {
- return cloneIfLocalBinder(widget.getEffectiveViewsLocked());
+ if (widget.appWidgetId == appWidgetId) {
+ outUpdates.clear();
+ for (int j = widget.updateTimes.size() - 1; j >= 0; j--) {
+ long time = widget.updateTimes.valueAt(j);
+ if (time <= updateTime) {
+ continue;
+ }
+ int id = widget.updateTimes.keyAt(j);
+ final PendingHostUpdate update;
+ switch (id) {
+ case ID_PROVIDER_CHANGED:
+ update = PendingHostUpdate.providerChanged(
+ appWidgetId, widget.provider.info);
+ break;
+ case ID_VIEWS_UPDATE:
+ update = PendingHostUpdate.updateAppWidget(appWidgetId,
+ cloneIfLocalBinder(widget.getEffectiveViewsLocked()));
+ break;
+ default:
+ update = PendingHostUpdate.viewDataChanged(appWidgetId, id);
+ }
+ outUpdates.put(time, update);
+ }
+ return true;
}
}
- return null;
+ return false;
}
@Override
@@ -3856,6 +3903,10 @@
}
}
+ // These can be any constants that would not collide with a resource id.
+ private static final int ID_VIEWS_UPDATE = 0;
+ private static final int ID_PROVIDER_CHANGED = 1;
+
private static final class Widget {
int appWidgetId;
int restoredId; // tracking & remapping any restored state
@@ -3864,7 +3915,8 @@
RemoteViews maskedViews;
Bundle options;
Host host;
- long lastUpdateTime;
+ // timestamps for various operations
+ SparseLongArray updateTimes = new SparseLongArray(2);
@Override
public String toString() {
diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java
index 59e4f28..baa0bbe 100644
--- a/services/core/java/com/android/server/BluetoothManagerService.java
+++ b/services/core/java/com/android/server/BluetoothManagerService.java
@@ -425,6 +425,24 @@
return false;
}
+ public int getState() {
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
+ (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getState(): not allowed for non-active and non system user");
+ return BluetoothAdapter.STATE_OFF;
+ }
+
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) return mBluetooth.getState();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "getState()", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ return BluetoothAdapter.STATE_OFF;
+ }
+
class ClientDeathRecipient implements IBinder.DeathRecipient {
public void binderDied() {
if (DBG) Slog.d(TAG, "Binder is dead - unregister Ble App");
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 130fb7c..e65b50c 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -797,6 +797,17 @@
return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId());
}
+ // Used only for testing.
+ // TODO: Delete this and either:
+ // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires
+ // changing ContentResolver to make registerContentObserver non-final).
+ // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
+ // by subclassing SettingsObserver.
+ @VisibleForTesting
+ void updateMobileDataAlwaysOn() {
+ mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
+ }
+
private void handleMobileDataAlwaysOn() {
final boolean enable = (Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 0) == 1);
@@ -2161,9 +2172,11 @@
loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
break;
}
- setProvNotificationVisibleIntent(true, netId, NotificationType.SIGN_IN,
- nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(),
- (PendingIntent)msg.obj, nai.networkMisc.explicitlySelected);
+ if (!nai.networkMisc.provisioningNotificationDisabled) {
+ setProvNotificationVisibleIntent(true, netId, NotificationType.SIGN_IN,
+ nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(),
+ (PendingIntent)msg.obj, nai.networkMisc.explicitlySelected);
+ }
}
break;
}
@@ -2564,6 +2577,7 @@
PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+
setProvNotificationVisibleIntent(true, nai.network.netId, NotificationType.NO_INTERNET,
nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(), pendingIntent, true);
}
diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java
index 1653db9..6f8edec 100644
--- a/services/core/java/com/android/server/MasterClearReceiver.java
+++ b/services/core/java/com/android/server/MasterClearReceiver.java
@@ -47,6 +47,7 @@
final String reason = intent.getStringExtra(Intent.EXTRA_REASON);
final boolean wipeExternalStorage = intent.getBooleanExtra(
Intent.EXTRA_WIPE_EXTERNAL_STORAGE, false);
+ final boolean forceWipe = intent.getBooleanExtra(Intent.EXTRA_FORCE_MASTER_CLEAR, false);
Slog.w(TAG, "!!! FACTORY RESET !!!");
// The reboot call is blocking, so we need to do it on another thread.
@@ -54,7 +55,7 @@
@Override
public void run() {
try {
- RecoverySystem.rebootWipeUserData(context, shutdown, reason);
+ RecoverySystem.rebootWipeUserData(context, shutdown, reason, forceWipe);
Log.wtf(TAG, "Still running after master clear?!");
} catch (IOException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 680547a..e233b1c 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -26,6 +26,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.Settings;
import android.service.persistentdata.IPersistentDataBlockService;
import android.service.persistentdata.PersistentDataBlockManager;
import android.util.Slog;
@@ -155,6 +156,15 @@
"Only the Admin user is allowed to change OEM unlock state");
}
}
+
+ private void enforceFactoryResetAllowed() {
+ final boolean isOemUnlockRestricted = UserManager.get(mContext)
+ .hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET);
+ if (isOemUnlockRestricted) {
+ throw new SecurityException("OEM unlock is disallowed by DISALLOW_FACTORY_RESET");
+ }
+ }
+
private int getTotalDataSizeLocked(DataInputStream inputStream) throws IOException {
// skip over checksum
inputStream.skipBytes(DIGEST_SIZE_BYTES);
@@ -447,14 +457,24 @@
}
@Override
- public void setOemUnlockEnabled(boolean enabled) {
+ public void setOemUnlockEnabled(boolean enabled) throws SecurityException {
// do not allow monkey to flip the flag
if (ActivityManager.isUserAMonkey()) {
return;
}
+
enforceOemUnlockWritePermission();
enforceIsAdmin();
+ if (enabled) {
+ // Do not allow oem unlock to be enabled if it has been disallowed.
+ if (Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.OEM_UNLOCK_DISALLOWED, 0) == 1) {
+ throw new SecurityException(
+ "OEM unlock has been disallowed by OEM_UNLOCK_DISALLOWED.");
+ }
+ enforceFactoryResetAllowed();
+ }
synchronized (mLock) {
doSetOemUnlockEnabledLocked(enabled);
computeAndWriteDigestLocked();
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 159a1bf..1a8076d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9260,9 +9260,6 @@
// sense, so turn off auto-remove.
intent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
}
- } else if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
- // Must be a new task.
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (!comp.equals(mLastAddedTaskComponent) || callingUid != mLastAddedTaskUid) {
mLastAddedTaskActivity = null;
@@ -18809,8 +18806,9 @@
/**
* Decide based on the configuration whether we should shouw the ANR,
- * crash, etc dialogs. The idea is that if there is no affordnace to
- * press the on-screen buttons, we shouldn't show the dialog.
+ * crash, etc dialogs. The idea is that if there is no affordence to
+ * press the on-screen buttons, or the user experience would be more
+ * greatly impacted than the crash itself, we shouldn't show the dialog.
*
* A thought: SystemUI might also want to get told about this, the Power
* dialog / global actions also might want different behaviors.
@@ -18819,9 +18817,10 @@
final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS
&& config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
&& config.navigation == Configuration.NAVIGATION_NONAV);
- final boolean uiIsNotCarType = !((config.uiMode & Configuration.UI_MODE_TYPE_MASK)
- == Configuration.UI_MODE_TYPE_CAR);
- return inputMethodExists && uiIsNotCarType && !inVrMode;
+ int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK;
+ final boolean uiModeSupportsDialogs = (modeType != Configuration.UI_MODE_TYPE_CAR
+ && !(modeType == Configuration.UI_MODE_TYPE_WATCH && "user".equals(Build.TYPE)));
+ return inputMethodExists && uiModeSupportsDialogs && !inVrMode;
}
@Override
@@ -21286,6 +21285,11 @@
Slog.w(TAG, "No user info for user #" + targetUserId);
return false;
}
+ if (!targetUserInfo.isDemo() && UserManager.isDeviceInDemoMode(mContext)) {
+ Slog.w(TAG, "Cannot switch to non-demo user #" + targetUserId
+ + " when device is in demo mode");
+ return false;
+ }
if (!targetUserInfo.supportsSwitchTo()) {
Slog.w(TAG, "Cannot switch to User #" + targetUserId + ": not supported");
return false;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 738622fd..be4e223 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -870,6 +870,8 @@
}
}
}
+ // Send launch end powerhint when idle
+ mService.mActivityStarter.sendPowerHintForLaunchEndIfNeeded();
return true;
}
@@ -2742,6 +2744,9 @@
}
}
+ // Send launch end powerhint before going sleep
+ mService.mActivityStarter.sendPowerHintForLaunchEndIfNeeded();
+
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
for (int stackNdx = stacks.size() - 1; stackNdx >= 0; --stackNdx) {
@@ -4409,6 +4414,7 @@
// Work Challenge is present) let startActivityInPackage handle the intercepting.
if (!mService.mUserController.shouldConfirmCredentials(task.userId)
&& task.getRootActivity() != null) {
+ mService.mActivityStarter.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */);
mActivityMetricsLogger.notifyActivityLaunching();
mService.moveTaskToFrontLocked(task.taskId, 0, bOptions);
mActivityMetricsLogger.notifyActivityLaunched(ActivityManager.START_TASK_TO_FRONT,
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index e425484..a5a7db9 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -102,6 +102,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.PowerManagerInternal;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -173,6 +174,7 @@
private boolean mNoAnimation;
private boolean mKeepCurTransition;
private boolean mAvoidMoveToFront;
+ private boolean mPowerHintSent;
private IVoiceInteractionSession mVoiceSession;
private IVoiceInteractor mVoiceInteractor;
@@ -944,6 +946,28 @@
return START_SUCCESS;
}
+ void sendPowerHintForLaunchStartIfNeeded(boolean forceSend) {
+ // Trigger launch power hint if activity being launched is not in the current task
+ final ActivityStack focusStack = mSupervisor.getFocusedStack();
+ final ActivityRecord curTop = (focusStack == null)
+ ? null : focusStack.topRunningNonDelayedActivityLocked(mNotTop);
+ if ((forceSend || (!mPowerHintSent && curTop != null &&
+ curTop.task != null && mStartActivity != null &&
+ curTop.task != mStartActivity.task )) &&
+ mService.mLocalPowerManager != null) {
+ mService.mLocalPowerManager.powerHint(PowerManagerInternal.POWER_HINT_LAUNCH, 1);
+ mPowerHintSent = true;
+ }
+ }
+
+ void sendPowerHintForLaunchEndIfNeeded() {
+ // Trigger launch power hint if activity is launched
+ if (mPowerHintSent && mService.mLocalPowerManager != null) {
+ mService.mLocalPowerManager.powerHint(PowerManagerInternal.POWER_HINT_LAUNCH, 0);
+ mPowerHintSent = false;
+ }
+ }
+
private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask) {
@@ -1005,6 +1029,8 @@
}
}
+ sendPowerHintForLaunchStartIfNeeded(false /* forceSend */);
+
mReusedActivity = setTargetStackAndMoveToFrontIfNeeded(mReusedActivity);
if ((mStartFlags & START_FLAG_ONLY_IF_NEEDED) != 0) {
@@ -1126,6 +1152,9 @@
ActivityStack.logStartActivity(
EventLogTags.AM_CREATE_ACTIVITY, mStartActivity, mStartActivity.task);
mTargetStack.mLastPausedActivity = null;
+
+ sendPowerHintForLaunchStartIfNeeded(false /* forceSend */);
+
mTargetStack.startActivityLocked(mStartActivity, newTask, mKeepCurTransition, mOptions);
if (mDoResume) {
if (!mLaunchTaskBehind) {
diff --git a/services/core/java/com/android/server/am/RetailDemoModeService.java b/services/core/java/com/android/server/am/RetailDemoModeService.java
new file mode 100644
index 0000000..6a5ec96
--- /dev/null
+++ b/services/core/java/com/android/server/am/RetailDemoModeService.java
@@ -0,0 +1,318 @@
+/*
+ * 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.am;
+
+import android.app.ActivityManagerNative;
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.R;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+import java.io.File;
+
+public class RetailDemoModeService extends SystemService {
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = RetailDemoModeService.class.getSimpleName();
+ private static final String DEMO_USER_NAME = "Demo";
+ private static final String ACTION_RESET_DEMO = "com.android.server.am.ACTION_RESET_DEMO";
+
+ private static final int MSG_TURN_SCREEN_ON = 0;
+ private static final int MSG_INACTIVITY_TIME_OUT = 1;
+ private static final int MSG_START_NEW_SESSION = 2;
+
+ private static final long SCREEN_WAKEUP_DELAY = 2500;
+ private static final long USER_INACTIVITY_TIMEOUT = 30000;
+
+ boolean mDeviceInDemoMode = false;
+ private ActivityManagerService mAms;
+ private NotificationManager mNm;
+ private UserManager mUm;
+ private PowerManager mPm;
+ private PowerManager.WakeLock mWakeLock;
+ Handler mHandler;
+ private ServiceThread mHandlerThread;
+ private PendingIntent mResetDemoPendingIntent;
+
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mDeviceInDemoMode) {
+ return;
+ }
+ switch (intent.getAction()) {
+ case Intent.ACTION_SCREEN_OFF:
+ mHandler.removeMessages(MSG_TURN_SCREEN_ON);
+ mHandler.sendEmptyMessageDelayed(MSG_TURN_SCREEN_ON, SCREEN_WAKEUP_DELAY);
+ break;
+ case ACTION_RESET_DEMO:
+ mHandler.sendEmptyMessage(MSG_START_NEW_SESSION);
+ break;
+ }
+ }
+ };
+
+ final class MainHandler extends Handler {
+
+ MainHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TURN_SCREEN_ON:
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ mWakeLock.acquire();
+ break;
+ case MSG_INACTIVITY_TIME_OUT:
+ IPackageManager pm = AppGlobals.getPackageManager();
+ int enabledState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+ String demoLauncherComponent = getContext().getResources()
+ .getString(R.string.config_demoModeLauncherComponent);
+ try {
+ enabledState = pm.getComponentEnabledSetting(
+ ComponentName.unflattenFromString(demoLauncherComponent),
+ getActivityManager().getCurrentUser().id);
+ } catch (RemoteException exc) {
+ // XXX: shouldn't happen
+ }
+ if (enabledState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
+ Slog.i(TAG, "Restarting session due to user inactivity timeout");
+ sendEmptyMessage(MSG_START_NEW_SESSION);
+ }
+ break;
+ case MSG_START_NEW_SESSION:
+ if (DEBUG) {
+ Slog.d(TAG, "Switching to a new demo user");
+ }
+ removeMessages(MSG_START_NEW_SESSION);
+ UserInfo demoUser = getUserManager().createUser(DEMO_USER_NAME,
+ UserInfo.FLAG_DEMO | UserInfo.FLAG_EPHEMERAL);
+ if (demoUser != null) {
+ setupDemoUser(demoUser);
+ getActivityManager().switchUser(demoUser.id);
+ }
+ break;
+ }
+ }
+ }
+
+ public RetailDemoModeService(Context context) {
+ super(context);
+ }
+
+ private Notification createResetNotification() {
+ return new Notification.Builder(getContext())
+ .setContentTitle(getContext().getString(R.string.reset_retail_demo_mode_title))
+ .setContentText(getContext().getString(R.string.reset_retail_demo_mode_text))
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.platlogo)
+ .setShowWhen(false)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setContentIntent(getResetDemoPendingIntent())
+ .setColor(getContext().getColor(R.color.system_notification_accent_color))
+ .build();
+ }
+
+ private PendingIntent getResetDemoPendingIntent() {
+ if (mResetDemoPendingIntent == null) {
+ Intent intent = new Intent(ACTION_RESET_DEMO);
+ mResetDemoPendingIntent = PendingIntent.getBroadcast(getContext(), 0, intent, 0);
+ }
+ return mResetDemoPendingIntent;
+ }
+
+ void setupDemoUser(UserInfo userInfo) {
+ UserManager um = getUserManager();
+ UserHandle user = UserHandle.of(userInfo.id);
+ LockPatternUtils lockPatternUtils = new LockPatternUtils(getContext());
+ lockPatternUtils.setLockScreenDisabled(true, userInfo.id);
+ um.setUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, true, user);
+ um.setUserRestriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, true, user);
+ um.setUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS, true, user);
+ um.setUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, true, user);
+ Settings.Secure.putIntForUser(getContext().getContentResolver(),
+ Settings.Secure.SKIP_FIRST_USE_HINTS, 1, userInfo.id);
+ }
+
+ private ActivityManagerService getActivityManager() {
+ if (mAms == null) {
+ mAms = (ActivityManagerService) ActivityManagerNative.getDefault();
+ }
+ return mAms;
+ }
+
+ private UserManager getUserManager() {
+ if (mUm == null) {
+ mUm = getContext().getSystemService(UserManager.class);
+ }
+ return mUm;
+ }
+
+ private void registerSettingsChangeObserver() {
+ final Uri deviceDemoModeUri = Settings.Global.getUriFor(Settings.Global.DEVICE_DEMO_MODE);
+ final Uri deviceProvisionedUri = Settings.Global.getUriFor(
+ Settings.Global.DEVICE_PROVISIONED);
+ final ContentResolver cr = getContext().getContentResolver();
+ final ContentObserver deviceDemoModeSettingObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri, int userId) {
+ if (deviceDemoModeUri.equals(uri)) {
+ mDeviceInDemoMode = UserManager.isDeviceInDemoMode(getContext());
+ if (mDeviceInDemoMode) {
+ mHandler.sendEmptyMessage(MSG_START_NEW_SESSION);
+ } else if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+ // If device is provisioned and left demo mode - run the cleanup in demo folder
+ if (!mDeviceInDemoMode && isDeviceProvisioned()) {
+ // Run on the bg thread to not block the fg thread
+ BackgroundThread.getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (!deleteDemoFolderContents()) {
+ Slog.w(TAG, "Failed to delete demo folder contents");
+ }
+ }
+ });
+ }
+ }
+ };
+ cr.registerContentObserver(deviceDemoModeUri, false, deviceDemoModeSettingObserver,
+ UserHandle.USER_SYSTEM);
+ cr.registerContentObserver(deviceProvisionedUri, false, deviceDemoModeSettingObserver,
+ UserHandle.USER_SYSTEM);
+ }
+
+ boolean isDeviceProvisioned() {
+ return Settings.Global.getInt(
+ getContext().getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+ }
+
+ private boolean deleteDemoFolderContents() {
+ File dir = Environment.getDataPreloadsDemoDirectory();
+ Slog.i(TAG, "Deleting contents of " + dir);
+ return FileUtils.deleteContents(dir);
+ }
+
+ private void registerBroadcastReceiver() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(ACTION_RESET_DEMO);
+ getContext().registerReceiver(mBroadcastReceiver, filter);
+ }
+
+ @Override
+ public void onStart() {
+ if (DEBUG) {
+ Slog.d(TAG, "Service starting up");
+ }
+ mHandlerThread = new ServiceThread(TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND,
+ false);
+ mHandlerThread.start();
+ mHandler = new MainHandler(mHandlerThread.getLooper());
+ publishLocalService(RetailDemoModeServiceInternal.class, mLocalService);
+ }
+
+ @Override
+ public void onBootPhase(int bootPhase) {
+ if (bootPhase != PHASE_THIRD_PARTY_APPS_CAN_START) {
+ return;
+ }
+ mPm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
+ mWakeLock = mPm
+ .newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
+ mNm = NotificationManager.from(getContext());
+
+ if (UserManager.isDeviceInDemoMode(getContext())) {
+ mDeviceInDemoMode = true;
+ mHandler.sendEmptyMessage(MSG_START_NEW_SESSION);
+ }
+ registerSettingsChangeObserver();
+ registerBroadcastReceiver();
+ }
+
+ @Override
+ public void onSwitchUser(int userId) {
+ if (!mDeviceInDemoMode) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "onSwitchUser: " + userId);
+ }
+ UserInfo ui = getUserManager().getUserInfo(userId);
+ if (!ui.isDemo()) {
+ Slog.wtf(TAG, "Should not allow switch to non-demo user in demo mode");
+ return;
+ }
+ if (!mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ mNm.notifyAsUser(TAG, 1, createResetNotification(), UserHandle.of(userId));
+ }
+
+ public RetailDemoModeServiceInternal mLocalService = new RetailDemoModeServiceInternal() {
+ private static final long USER_ACTIVITY_DEBOUNCE_TIME = 2000;
+ private long mLastUserActivityTime = 0;
+
+ @Override
+ public void onUserActivity() {
+ if (!mDeviceInDemoMode) {
+ return;
+ }
+ long timeOfActivity = SystemClock.uptimeMillis();
+ if (timeOfActivity < mLastUserActivityTime + USER_ACTIVITY_DEBOUNCE_TIME) {
+ return;
+ }
+ mLastUserActivityTime = timeOfActivity;
+ mHandler.removeMessages(MSG_INACTIVITY_TIME_OUT);
+ mHandler.sendEmptyMessageDelayed(MSG_INACTIVITY_TIME_OUT, USER_INACTIVITY_TIMEOUT);
+ }
+ };
+}
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl b/services/core/java/com/android/server/am/RetailDemoModeServiceInternal.java
similarity index 64%
copy from telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
copy to services/core/java/com/android/server/am/RetailDemoModeServiceInternal.java
index b7e78d1..32de03a 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
+++ b/services/core/java/com/android/server/am/RetailDemoModeServiceInternal.java
@@ -1,22 +1,21 @@
/*
- * Copyright 2016, The Android Open Source Project
+ * 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
+ * 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.
+ * limitations under the License
*/
-package android.telecom;
+package com.android.server.am;
-/**
- * {@hide}
- */
-parcelable ParcelableCallAnalytics;
+public interface RetailDemoModeServiceInternal {
+ public void onUserActivity();
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 4380af3..2375d7a 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -224,6 +224,7 @@
private void finishUserBoot(UserState uss, IIntentReceiver resultTo) {
final int userId = uss.mHandle.getIdentifier();
+
Slog.d(TAG, "Finishing user boot " + userId);
synchronized (mService) {
// Bail if we ended up with a stale user
diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java
index 10e88e6..7022856 100644
--- a/services/core/java/com/android/server/am/UserSwitchingDialog.java
+++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java
@@ -68,6 +68,12 @@
String viewMessage;
if (UserManager.isSplitSystemUser() && newUser.id == UserHandle.USER_SYSTEM) {
viewMessage = res.getString(R.string.user_logging_out_message, oldUser.name);
+ } else if (UserManager.isDeviceInDemoMode(context)) {
+ if (oldUser.isDemo()) {
+ viewMessage = res.getString(R.string.demo_restarting_message);
+ } else {
+ viewMessage = res.getString(R.string.demo_starting_message);
+ }
} else {
viewMessage = res.getString(R.string.user_switching_message, newUser.name);
}
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index f4e1424..ddaebfa 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -66,7 +66,6 @@
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.WakeupMessage;
-import com.android.server.connectivity.NetworkAgentInfo;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -618,7 +617,7 @@
@Override
public void exit() {
- removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
+ removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
}
}
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 1012f9a..bef48d6 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -34,8 +34,6 @@
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.INetworkStatsService;
-import android.net.InterfaceConfiguration;
-import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -58,18 +56,20 @@
import android.telephony.CarrierConfigManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.TelephonyIntents;
-import com.android.internal.util.IState;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.IoThread;
+import com.android.server.connectivity.tethering.IControlsTethering;
+import com.android.server.connectivity.tethering.TetherInterfaceStateMachine;
import com.android.server.net.BaseNetworkObserver;
import java.io.FileDescriptor;
@@ -81,18 +81,16 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @hide
*
- * Timeout
- *
- * TODO - look for parent classes and code sharing
+ * This class holds much of the business logic to allow Android devices
+ * to act as IP gateways via USB, BT, and WiFi interfaces.
*/
-public class Tethering extends BaseNetworkObserver {
+public class Tethering extends BaseNetworkObserver implements IControlsTethering {
private final Context mContext;
private final static String TAG = "Tethering";
@@ -100,7 +98,7 @@
private final static boolean VDBG = false;
private static final Class[] messageClasses = {
- Tethering.class, TetherMasterSM.class, TetherInterfaceSM.class
+ Tethering.class, TetherMasterSM.class, TetherInterfaceStateMachine.class
};
private static final SparseArray<String> sMagicDecoderRing =
MessageUtils.findMessageNames(messageClasses);
@@ -126,17 +124,25 @@
private final INetworkStatsService mStatsService;
private final Looper mLooper;
- private HashMap<String, TetherInterfaceSM> mIfaces; // all tethered/tetherable ifaces
+ private static class TetherState {
+ public final TetherInterfaceStateMachine mStateMachine;
+ public int mLastState;
+ public int mLastError;
+ public TetherState(TetherInterfaceStateMachine sm) {
+ mStateMachine = sm;
+ // Assume all state machines start out available and with no errors.
+ mLastState = IControlsTethering.STATE_AVAILABLE;
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ }
+ }
+ private final ArrayMap<String, TetherState> mTetherStates;
- private BroadcastReceiver mStateReceiver;
+ private final BroadcastReceiver mStateReceiver;
// {@link ComponentName} of the Service used to run tether provisioning.
private static final ComponentName TETHER_SERVICE = ComponentName.unflattenFromString(Resources
.getSystem().getString(com.android.internal.R.string.config_wifi_tether_enable));
- private static final String USB_NEAR_IFACE_ADDR = "192.168.42.129";
- private static final int USB_PREFIX_LENGTH = 24;
-
// USB is 192.168.42.1 and 255.255.255.0
// Wifi is 192.168.43.1 and 255.255.255.0
// BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
@@ -166,6 +172,9 @@
private boolean mUsbTetherRequested; // true if USB tethering should be started
// when RNDIS is enabled
+ // True iff WiFi tethering should be started when soft AP is ready.
+ private boolean mWifiTetherRequested;
+
public Tethering(Context context, INetworkManagementService nmService,
INetworkStatsService statsService) {
mContext = context;
@@ -174,7 +183,7 @@
mPublicSync = new Object();
- mIfaces = new HashMap<String, TetherInterfaceSM>();
+ mTetherStates = new ArrayMap<>();
// make our own thread so we don't anr the system
mLooper = IoThread.get().getLooper();
@@ -187,6 +196,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_STATE);
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
mContext.registerReceiver(mStateReceiver, filter);
@@ -227,7 +237,7 @@
int ifaceTypes[] = mContext.getResources().getIntArray(
com.android.internal.R.array.config_tether_upstream_types);
- Collection<Integer> upstreamIfaceTypes = new ArrayList();
+ Collection<Integer> upstreamIfaceTypes = new ArrayList<>();
for (int i : ifaceTypes) {
upstreamIfaceTypes.add(new Integer(i));
}
@@ -248,34 +258,26 @@
// Never called directly: only called from interfaceLinkStateChanged.
// See NetlinkHandler.cpp:71.
if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
- boolean found = false;
- boolean usb = false;
synchronized (mPublicSync) {
- if (isWifi(iface)) {
- found = true;
- } else if (isUsb(iface)) {
- found = true;
- usb = true;
- } else if (isBluetooth(iface)) {
- found = true;
+ int interfaceType = ifaceNameToType(iface);
+ if (interfaceType == ConnectivityManager.TETHERING_INVALID) {
+ return;
}
- if (found == false) return;
- TetherInterfaceSM sm = mIfaces.get(iface);
+ TetherState tetherState = mTetherStates.get(iface);
if (up) {
- if (sm == null) {
- sm = new TetherInterfaceSM(iface, mLooper, usb);
- mIfaces.put(iface, sm);
- sm.start();
+ if (tetherState == null) {
+ trackNewTetherableInterface(iface, interfaceType);
}
} else {
- if (isUsb(iface)) {
+ if (interfaceType == ConnectivityManager.TETHERING_USB) {
// ignore usb0 down after enabling RNDIS
// we will handle disconnect in interfaceRemoved instead
if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
- } else if (sm != null) {
- sm.sendMessage(TetherInterfaceSM.CMD_INTERFACE_DOWN);
- mIfaces.remove(iface);
+ } else if (tetherState != null) {
+ tetherState.mStateMachine.sendMessage(
+ TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
+ mTetherStates.remove(iface);
}
}
}
@@ -295,7 +297,7 @@
}
}
- public boolean isWifi(String iface) {
+ private boolean isWifi(String iface) {
synchronized (mPublicSync) {
for (String regex : mTetherableWifiRegexs) {
if (iface.matches(regex)) return true;
@@ -304,7 +306,7 @@
}
}
- public boolean isBluetooth(String iface) {
+ private boolean isBluetooth(String iface) {
synchronized (mPublicSync) {
for (String regex : mTetherableBluetoothRegexs) {
if (iface.matches(regex)) return true;
@@ -313,35 +315,33 @@
}
}
+ private int ifaceNameToType(String iface) {
+ if (isWifi(iface)) {
+ return ConnectivityManager.TETHERING_WIFI;
+ } else if (isUsb(iface)) {
+ return ConnectivityManager.TETHERING_USB;
+ } else if (isBluetooth(iface)) {
+ return ConnectivityManager.TETHERING_BLUETOOTH;
+ }
+ return ConnectivityManager.TETHERING_INVALID;
+ }
+
@Override
public void interfaceAdded(String iface) {
if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
- boolean found = false;
- boolean usb = false;
synchronized (mPublicSync) {
- if (isWifi(iface)) {
- found = true;
- }
- if (isUsb(iface)) {
- found = true;
- usb = true;
- }
- if (isBluetooth(iface)) {
- found = true;
- }
- if (found == false) {
+ int interfaceType = ifaceNameToType(iface);
+ if (interfaceType == ConnectivityManager.TETHERING_INVALID) {
if (VDBG) Log.d(TAG, iface + " is not a tetherable iface, ignoring");
return;
}
- TetherInterfaceSM sm = mIfaces.get(iface);
- if (sm != null) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ trackNewTetherableInterface(iface, interfaceType);
+ } else {
if (VDBG) Log.d(TAG, "active iface (" + iface + ") reported as added, ignoring");
- return;
}
- sm = new TetherInterfaceSM(iface, mLooper, usb);
- mIfaces.put(iface, sm);
- sm.start();
}
}
@@ -349,15 +349,15 @@
public void interfaceRemoved(String iface) {
if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
synchronized (mPublicSync) {
- TetherInterfaceSM sm = mIfaces.get(iface);
- if (sm == null) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
if (VDBG) {
Log.e(TAG, "attempting to remove unknown iface (" + iface + "), ignoring");
}
return;
}
- sm.sendMessage(TetherInterfaceSM.CMD_INTERFACE_DOWN);
- mIfaces.remove(iface);
+ tetherState.mStateMachine.sendMessage(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
+ mTetherStates.remove(iface);
}
}
@@ -413,24 +413,19 @@
* for the specified interface.
*/
private void enableTetheringInternal(int type, boolean enable, ResultReceiver receiver) {
- boolean isProvisioningRequired = isTetherProvisioningRequired();
+ boolean isProvisioningRequired = enable && isTetherProvisioningRequired();
+ int result;
switch (type) {
case ConnectivityManager.TETHERING_WIFI:
- final WifiManager wifiManager =
- (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
- if (wifiManager.setWifiApEnabled(null, enable)) {
- sendTetherResult(receiver, ConnectivityManager.TETHER_ERROR_NO_ERROR);
- if (enable && isProvisioningRequired) {
- scheduleProvisioningRechecks(type);
- }
- } else{
- sendTetherResult(receiver, ConnectivityManager.TETHER_ERROR_MASTER_ERROR);
+ result = setWifiTethering(enable);
+ if (isProvisioningRequired && result == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ scheduleProvisioningRechecks(type);
}
+ sendTetherResult(receiver, result);
break;
case ConnectivityManager.TETHERING_USB:
- int result = setUsbTethering(enable);
- if (enable && isProvisioningRequired &&
- result == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ result = setUsbTethering(enable);
+ if (isProvisioningRequired && result == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
scheduleProvisioningRechecks(type);
}
sendTetherResult(receiver, result);
@@ -450,6 +445,20 @@
}
}
+ private int setWifiTethering(final boolean enable) {
+ synchronized (mPublicSync) {
+ // Note that we're maintaining a predicate that mWifiTetherRequested always matches
+ // our last request to WifiManager re: its AP enabled status.
+ mWifiTetherRequested = enable;
+ final WifiManager wifiManager =
+ (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager.setWifiApEnabled(null /* use existing wifi config */, enable)) {
+ return ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ }
+ return ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+ }
+ }
+
private void setBluetoothTethering(final boolean enable, final ResultReceiver receiver) {
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null || !adapter.isEnabled()) {
@@ -576,62 +585,62 @@
public int tether(String iface) {
if (DBG) Log.d(TAG, "Tethering " + iface);
- TetherInterfaceSM sm = null;
synchronized (mPublicSync) {
- sm = mIfaces.get(iface);
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Tether an unknown iface :" + iface + ", ignoring");
+ return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ // Ignore the error status of the interface. If the interface is available,
+ // the errors are referring to past tethering attempts anyway.
+ if (tetherState.mLastState != IControlsTethering.STATE_AVAILABLE) {
+ Log.e(TAG, "Tried to Tether an unavailable iface :" + iface + ", ignoring");
+ return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ tetherState.mStateMachine.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
- if (sm == null) {
- Log.e(TAG, "Tried to Tether an unknown iface :" + iface + ", ignoring");
- return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
- }
- if (!sm.isAvailable() && !sm.isErrored()) {
- Log.e(TAG, "Tried to Tether an unavailable iface :" + iface + ", ignoring");
- return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
- }
- sm.sendMessage(TetherInterfaceSM.CMD_TETHER_REQUESTED);
- return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
public int untether(String iface) {
if (DBG) Log.d(TAG, "Untethering " + iface);
- TetherInterfaceSM sm = null;
synchronized (mPublicSync) {
- sm = mIfaces.get(iface);
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
+ return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ if (tetherState.mLastState != IControlsTethering.STATE_TETHERED) {
+ Log.e(TAG, "Tried to untether an untethered iface :" + iface + ", ignoring");
+ return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ tetherState.mStateMachine.sendMessage(
+ TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
+ return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
- if (sm == null) {
- Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
- return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
- }
- if (sm.isErrored()) {
- Log.e(TAG, "Tried to Untethered an errored iface :" + iface + ", ignoring");
- return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
- }
- sm.sendMessage(TetherInterfaceSM.CMD_TETHER_UNREQUESTED);
- return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
public void untetherAll() {
- if (DBG) Log.d(TAG, "Untethering " + mIfaces);
- for (String iface : mIfaces.keySet()) {
- untether(iface);
+ synchronized (mPublicSync) {
+ if (DBG) Log.d(TAG, "Untethering " + mTetherStates.keySet());
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ untether(mTetherStates.keyAt(i));
+ }
}
}
public int getLastTetherError(String iface) {
- TetherInterfaceSM sm = null;
synchronized (mPublicSync) {
- sm = mIfaces.get(iface);
- if (sm == null) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
Log.e(TAG, "Tried to getLastTetherError on an unknown iface :" + iface +
", ignoring");
return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
}
- return sm.getLastError();
+ return tetherState.mLastError;
}
}
- // TODO - move all private methods used only by the state machine into the state machine
- // to clarify what needs synchronized protection.
private void sendTetherStateChangedBroadcast() {
if (!getConnectivityManager().isTetheringSupported()) return;
@@ -644,24 +653,22 @@
boolean bluetoothTethered = false;
synchronized (mPublicSync) {
- Set ifaces = mIfaces.keySet();
- for (Object iface : ifaces) {
- TetherInterfaceSM sm = mIfaces.get(iface);
- if (sm != null) {
- if (sm.isErrored()) {
- erroredList.add((String)iface);
- } else if (sm.isAvailable()) {
- availableList.add((String)iface);
- } else if (sm.isTethered()) {
- if (isUsb((String)iface)) {
- usbTethered = true;
- } else if (isWifi((String)iface)) {
- wifiTethered = true;
- } else if (isBluetooth((String)iface)) {
- bluetoothTethered = true;
- }
- activeList.add((String)iface);
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ String iface = mTetherStates.keyAt(i);
+ if (tetherState.mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ erroredList.add(iface);
+ } else if (tetherState.mLastState == IControlsTethering.STATE_AVAILABLE) {
+ availableList.add(iface);
+ } else if (tetherState.mLastState == IControlsTethering.STATE_TETHERED) {
+ if (isUsb(iface)) {
+ usbTethered = true;
+ } else if (isWifi(iface)) {
+ wifiTethered = true;
+ } else if (isBluetooth(iface)) {
+ bluetoothTethered = true;
}
+ activeList.add(iface);
}
}
}
@@ -770,7 +777,7 @@
mRndisEnabled = intent.getBooleanExtra(UsbManager.USB_FUNCTION_RNDIS, false);
// start tethering if we have a request pending
if (usbConnected && mRndisEnabled && mUsbTetherRequested) {
- tetherUsb(true);
+ tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
}
mUsbTetherRequested = false;
}
@@ -782,69 +789,72 @@
if (VDBG) Log.d(TAG, "Tethering got CONNECTIVITY_ACTION");
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_UPSTREAM_CHANGED);
}
+ } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
+ synchronized (Tethering.this.mPublicSync) {
+ if (!mWifiTetherRequested) {
+ // We only care when we're trying to tether via our WiFi interface.
+ return;
+ }
+ int curState = intent.getIntExtra(WifiManager.EXTRA_WIFI_AP_STATE,
+ WifiManager.WIFI_AP_STATE_DISABLED);
+ switch (curState) {
+ case WifiManager.WIFI_AP_STATE_ENABLING:
+ // We can see this state on the way to both enabled and failure states.
+ break;
+ case WifiManager.WIFI_AP_STATE_ENABLED:
+ // Tell an appropriate interface state machine that it should tether.
+ tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_WIFI);
+ break;
+ case WifiManager.WIFI_AP_STATE_DISABLED:
+ case WifiManager.WIFI_AP_STATE_DISABLING:
+ case WifiManager.WIFI_AP_STATE_FAILED:
+ default:
+ if (DBG) {
+ Log.d(TAG, "Canceling WiFi tethering request - AP_STATE=" +
+ curState);
+ }
+ // Tell an appropriate interface state machine that
+ // it needs to tear itself down.
+ tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_WIFI);
+ setWifiTethering(false);
+ break;
+ }
+ }
} else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
updateConfiguration();
}
}
}
- private void tetherUsb(boolean enable) {
- if (VDBG) Log.d(TAG, "tetherUsb " + enable);
+ private void tetherMatchingInterfaces(boolean enable, int interfaceType) {
+ if (VDBG) Log.d(TAG, "tetherMatchingInterfaces(" + enable + ", " + interfaceType + ")");
- String[] ifaces = new String[0];
+ String[] ifaces = null;
try {
ifaces = mNMService.listInterfaces();
} catch (Exception e) {
Log.e(TAG, "Error listing Interfaces", e);
return;
}
- for (String iface : ifaces) {
- if (isUsb(iface)) {
- int result = (enable ? tether(iface) : untether(iface));
- if (result == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
- return;
+ String chosenIface = null;
+ if (ifaces != null) {
+ for (String iface : ifaces) {
+ if (ifaceNameToType(iface) == interfaceType) {
+ chosenIface = iface;
+ break;
}
}
}
- Log.e(TAG, "unable start or stop USB tethering");
- }
-
- // configured when we start tethering and unconfig'd on error or conclusion
- private boolean configureUsbIface(boolean enabled) {
- if (VDBG) Log.d(TAG, "configureUsbIface(" + enabled + ")");
-
- // toggle the USB interfaces
- String[] ifaces = new String[0];
- try {
- ifaces = mNMService.listInterfaces();
- } catch (Exception e) {
- Log.e(TAG, "Error listing Interfaces", e);
- return false;
+ if (chosenIface == null) {
+ Log.e(TAG, "could not find iface of type " + interfaceType);
+ return;
}
- for (String iface : ifaces) {
- if (isUsb(iface)) {
- InterfaceConfiguration ifcg = null;
- try {
- ifcg = mNMService.getInterfaceConfig(iface);
- if (ifcg != null) {
- InetAddress addr = NetworkUtils.numericToInetAddress(USB_NEAR_IFACE_ADDR);
- ifcg.setLinkAddress(new LinkAddress(addr, USB_PREFIX_LENGTH));
- if (enabled) {
- ifcg.setInterfaceUp();
- } else {
- ifcg.setInterfaceDown();
- }
- ifcg.clearFlag("running");
- mNMService.setInterfaceConfig(iface, ifcg);
- }
- } catch (Exception e) {
- Log.e(TAG, "Error configuring interface " + iface, e);
- return false;
- }
- }
- }
- return true;
+ int result = (enable ? tether(chosenIface) : untether(chosenIface));
+ if (result != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start or stop tethering on iface " + chosenIface);
+ return;
+ }
}
// TODO - return copies so people can't tamper
@@ -869,7 +879,7 @@
if (mRndisEnabled) {
final long ident = Binder.clearCallingIdentity();
try {
- tetherUsb(true);
+ tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -880,7 +890,7 @@
} else {
final long ident = Binder.clearCallingIdentity();
try {
- tetherUsb(false);
+ tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -952,37 +962,27 @@
public String[] getTetheredIfaces() {
ArrayList<String> list = new ArrayList<String>();
synchronized (mPublicSync) {
- Set keys = mIfaces.keySet();
- for (Object key : keys) {
- TetherInterfaceSM sm = mIfaces.get(key);
- if (sm.isTethered()) {
- list.add((String)key);
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.mLastState == IControlsTethering.STATE_TETHERED) {
+ list.add(mTetherStates.keyAt(i));
}
}
}
- String[] retVal = new String[list.size()];
- for (int i=0; i < list.size(); i++) {
- retVal[i] = list.get(i);
- }
- return retVal;
+ return list.toArray(new String[list.size()]);
}
public String[] getTetherableIfaces() {
ArrayList<String> list = new ArrayList<String>();
synchronized (mPublicSync) {
- Set keys = mIfaces.keySet();
- for (Object key : keys) {
- TetherInterfaceSM sm = mIfaces.get(key);
- if (sm.isAvailable()) {
- list.add((String)key);
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.mLastState == IControlsTethering.STATE_AVAILABLE) {
+ list.add(mTetherStates.keyAt(i));
}
}
}
- String[] retVal = new String[list.size()];
- for (int i=0; i < list.size(); i++) {
- retVal[i] = list.get(i);
- }
- return retVal;
+ return list.toArray(new String[list.size()]);
}
public String[] getTetheredDhcpRanges() {
@@ -992,19 +992,14 @@
public String[] getErroredIfaces() {
ArrayList<String> list = new ArrayList<String>();
synchronized (mPublicSync) {
- Set keys = mIfaces.keySet();
- for (Object key : keys) {
- TetherInterfaceSM sm = mIfaces.get(key);
- if (sm.isErrored()) {
- list.add((String)key);
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ list.add(mTetherStates.keyAt(i));
}
}
}
- String[] retVal = new String[list.size()];
- for (int i= 0; i< list.size(); i++) {
- retVal[i] = list.get(i);
- }
- return retVal;
+ return list.toArray(new String[list.size()]);
}
private void maybeLogMessage(State state, int what) {
@@ -1014,401 +1009,6 @@
}
}
- class TetherInterfaceSM extends StateMachine {
- private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100;
- // notification from the master SM that it's not in tether mode
- static final int CMD_TETHER_MODE_DEAD = BASE_IFACE + 1;
- // request from the user that it wants to tether
- static final int CMD_TETHER_REQUESTED = BASE_IFACE + 2;
- // request from the user that it wants to untether
- static final int CMD_TETHER_UNREQUESTED = BASE_IFACE + 3;
- // notification that this interface is down
- static final int CMD_INTERFACE_DOWN = BASE_IFACE + 4;
- // notification that this interface is up
- static final int CMD_INTERFACE_UP = BASE_IFACE + 5;
- // notification from the master SM that it had an error turning on cellular dun
- static final int CMD_CELL_DUN_ERROR = BASE_IFACE + 6;
- // notification from the master SM that it had trouble enabling IP Forwarding
- static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IFACE + 7;
- // notification from the master SM that it had trouble disabling IP Forwarding
- static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IFACE + 8;
- // notification from the master SM that it had trouble starting tethering
- static final int CMD_START_TETHERING_ERROR = BASE_IFACE + 9;
- // notification from the master SM that it had trouble stopping tethering
- static final int CMD_STOP_TETHERING_ERROR = BASE_IFACE + 10;
- // notification from the master SM that it had trouble setting the DNS forwarders
- static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IFACE + 11;
- // the upstream connection has changed
- static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IFACE + 12;
-
- private State mDefaultState;
-
- private State mInitialState;
- private State mStartingState;
- private State mTetheredState;
-
- private State mUnavailableState;
-
- private boolean mAvailable;
- private boolean mTethered;
- int mLastError;
-
- String mIfaceName;
- String mMyUpstreamIfaceName; // may change over time
-
- boolean mUsb;
-
- TetherInterfaceSM(String name, Looper looper, boolean usb) {
- super(name, looper);
- mIfaceName = name;
- mUsb = usb;
- setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
-
- mInitialState = new InitialState();
- addState(mInitialState);
- mStartingState = new StartingState();
- addState(mStartingState);
- mTetheredState = new TetheredState();
- addState(mTetheredState);
- mUnavailableState = new UnavailableState();
- addState(mUnavailableState);
-
- setInitialState(mInitialState);
- }
-
- public String toString() {
- String res = new String();
- res += mIfaceName + " - ";
- IState current = getCurrentState();
- if (current == mInitialState) res += "InitialState";
- if (current == mStartingState) res += "StartingState";
- if (current == mTetheredState) res += "TetheredState";
- if (current == mUnavailableState) res += "UnavailableState";
- if (mAvailable) res += " - Available";
- if (mTethered) res += " - Tethered";
- res += " - lastError =" + mLastError;
- return res;
- }
-
- public int getLastError() {
- synchronized (Tethering.this.mPublicSync) {
- return mLastError;
- }
- }
-
- private void setLastError(int error) {
- synchronized (Tethering.this.mPublicSync) {
- mLastError = error;
-
- if (isErrored()) {
- if (mUsb) {
- // note everything's been unwound by this point so nothing to do on
- // further error..
- Tethering.this.configureUsbIface(false);
- }
- }
- }
- }
-
- public boolean isAvailable() {
- synchronized (Tethering.this.mPublicSync) {
- return mAvailable;
- }
- }
-
- private void setAvailable(boolean available) {
- synchronized (Tethering.this.mPublicSync) {
- mAvailable = available;
- }
- }
-
- public boolean isTethered() {
- synchronized (Tethering.this.mPublicSync) {
- return mTethered;
- }
- }
-
- private void setTethered(boolean tethered) {
- synchronized (Tethering.this.mPublicSync) {
- mTethered = tethered;
- }
- }
-
- public boolean isErrored() {
- synchronized (Tethering.this.mPublicSync) {
- return (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR);
- }
- }
-
- class InitialState extends State {
- @Override
- public void enter() {
- setAvailable(true);
- setTethered(false);
- sendTetherStateChangedBroadcast();
- }
-
- @Override
- public boolean processMessage(Message message) {
- maybeLogMessage(this, message.what);
- boolean retValue = true;
- switch (message.what) {
- case CMD_TETHER_REQUESTED:
- setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED,
- TetherInterfaceSM.this);
- transitionTo(mStartingState);
- break;
- case CMD_INTERFACE_DOWN:
- transitionTo(mUnavailableState);
- break;
- default:
- retValue = false;
- break;
- }
- return retValue;
- }
- }
-
- class StartingState extends State {
- @Override
- public void enter() {
- setAvailable(false);
- if (mUsb) {
- if (!Tethering.this.configureUsbIface(true)) {
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED,
- TetherInterfaceSM.this);
- setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR);
-
- transitionTo(mInitialState);
- return;
- }
- }
- sendTetherStateChangedBroadcast();
-
- // Skipping StartingState
- transitionTo(mTetheredState);
- }
- @Override
- public boolean processMessage(Message message) {
- maybeLogMessage(this, message.what);
- boolean retValue = true;
- switch (message.what) {
- // maybe a parent class?
- case CMD_TETHER_UNREQUESTED:
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED,
- TetherInterfaceSM.this);
- if (mUsb) {
- if (!Tethering.this.configureUsbIface(false)) {
- setLastErrorAndTransitionToInitialState(
- ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR);
- break;
- }
- }
- transitionTo(mInitialState);
- break;
- case CMD_CELL_DUN_ERROR:
- case CMD_IP_FORWARDING_ENABLE_ERROR:
- case CMD_IP_FORWARDING_DISABLE_ERROR:
- case CMD_START_TETHERING_ERROR:
- case CMD_STOP_TETHERING_ERROR:
- case CMD_SET_DNS_FORWARDERS_ERROR:
- setLastErrorAndTransitionToInitialState(
- ConnectivityManager.TETHER_ERROR_MASTER_ERROR);
- break;
- case CMD_INTERFACE_DOWN:
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED,
- TetherInterfaceSM.this);
- transitionTo(mUnavailableState);
- break;
- default:
- retValue = false;
- }
- return retValue;
- }
- }
-
- class TetheredState extends State {
- @Override
- public void enter() {
- try {
- mNMService.tetherInterface(mIfaceName);
- } catch (Exception e) {
- Log.e(TAG, "Error Tethering: " + e.toString());
- setLastError(ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR);
-
- try {
- mNMService.untetherInterface(mIfaceName);
- } catch (Exception ee) {
- Log.e(TAG, "Error untethering after failure!" + ee.toString());
- }
- transitionTo(mInitialState);
- return;
- }
- if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
- setAvailable(false);
- setTethered(true);
- sendTetherStateChangedBroadcast();
- }
-
- private void cleanupUpstream() {
- if (mMyUpstreamIfaceName != null) {
- // note that we don't care about errors here.
- // sometimes interfaces are gone before we get
- // to remove their rules, which generates errors.
- // just do the best we can.
- try {
- // about to tear down NAT; gather remaining statistics
- mStatsService.forceUpdate();
- } catch (Exception e) {
- if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
- }
- try {
- mNMService.stopInterfaceForwarding(mIfaceName, mMyUpstreamIfaceName);
- } catch (Exception e) {
- if (VDBG) Log.e(
- TAG, "Exception in removeInterfaceForward: " + e.toString());
- }
- try {
- mNMService.disableNat(mIfaceName, mMyUpstreamIfaceName);
- } catch (Exception e) {
- if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
- }
- mMyUpstreamIfaceName = null;
- }
- return;
- }
-
- @Override
- public boolean processMessage(Message message) {
- maybeLogMessage(this, message.what);
- boolean retValue = true;
- boolean error = false;
- switch (message.what) {
- case CMD_TETHER_UNREQUESTED:
- case CMD_INTERFACE_DOWN:
- cleanupUpstream();
- try {
- mNMService.untetherInterface(mIfaceName);
- } catch (Exception e) {
- setLastErrorAndTransitionToInitialState(
- ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR);
- break;
- }
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED,
- TetherInterfaceSM.this);
- if (message.what == CMD_TETHER_UNREQUESTED) {
- if (mUsb) {
- if (!Tethering.this.configureUsbIface(false)) {
- setLastError(
- ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR);
- }
- }
- transitionTo(mInitialState);
- } else if (message.what == CMD_INTERFACE_DOWN) {
- transitionTo(mUnavailableState);
- }
- if (DBG) Log.d(TAG, "Untethered " + mIfaceName);
- break;
- case CMD_TETHER_CONNECTION_CHANGED:
- String newUpstreamIfaceName = (String)(message.obj);
- if ((mMyUpstreamIfaceName == null && newUpstreamIfaceName == null) ||
- (mMyUpstreamIfaceName != null &&
- mMyUpstreamIfaceName.equals(newUpstreamIfaceName))) {
- if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
- break;
- }
- cleanupUpstream();
- if (newUpstreamIfaceName != null) {
- try {
- mNMService.enableNat(mIfaceName, newUpstreamIfaceName);
- mNMService.startInterfaceForwarding(mIfaceName,
- newUpstreamIfaceName);
- } catch (Exception e) {
- Log.e(TAG, "Exception enabling Nat: " + e.toString());
- try {
- mNMService.disableNat(mIfaceName, newUpstreamIfaceName);
- } catch (Exception ee) {}
- try {
- mNMService.untetherInterface(mIfaceName);
- } catch (Exception ee) {}
-
- setLastError(ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR);
- transitionTo(mInitialState);
- return true;
- }
- }
- mMyUpstreamIfaceName = newUpstreamIfaceName;
- break;
- case CMD_CELL_DUN_ERROR:
- case CMD_IP_FORWARDING_ENABLE_ERROR:
- case CMD_IP_FORWARDING_DISABLE_ERROR:
- case CMD_START_TETHERING_ERROR:
- case CMD_STOP_TETHERING_ERROR:
- case CMD_SET_DNS_FORWARDERS_ERROR:
- error = true;
- // fall through
- case CMD_TETHER_MODE_DEAD:
- cleanupUpstream();
- try {
- mNMService.untetherInterface(mIfaceName);
- } catch (Exception e) {
- setLastErrorAndTransitionToInitialState(
- ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR);
- break;
- }
- if (error) {
- setLastErrorAndTransitionToInitialState(
- ConnectivityManager.TETHER_ERROR_MASTER_ERROR);
- break;
- }
- if (DBG) Log.d(TAG, "Tether lost upstream connection " + mIfaceName);
- sendTetherStateChangedBroadcast();
- if (mUsb) {
- if (!Tethering.this.configureUsbIface(false)) {
- setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR);
- }
- }
- transitionTo(mInitialState);
- break;
- default:
- retValue = false;
- break;
- }
- return retValue;
- }
- }
-
- class UnavailableState extends State {
- @Override
- public void enter() {
- setAvailable(false);
- setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
- setTethered(false);
- sendTetherStateChangedBroadcast();
- }
- @Override
- public boolean processMessage(Message message) {
- boolean retValue = true;
- switch (message.what) {
- case CMD_INTERFACE_UP:
- transitionTo(mInitialState);
- break;
- default:
- retValue = false;
- break;
- }
- return retValue;
- }
- }
-
- void setLastErrorAndTransitionToInitialState(int error) {
- setLastError(error);
- transitionTo(mInitialState);
- }
-
- }
-
/**
* A NetworkCallback class that relays information of interest to the
* tethering master state machine thread for subsequent processing.
@@ -1442,7 +1042,7 @@
* could/should be moved here.
*/
class UpstreamNetworkMonitor {
- final HashMap<Network, NetworkState> mNetworkMap = new HashMap();
+ final HashMap<Network, NetworkState> mNetworkMap = new HashMap<>();
NetworkCallback mDefaultNetworkCallback;
NetworkCallback mDunTetheringCallback;
@@ -1520,12 +1120,6 @@
static final int EVENT_UPSTREAM_LINKPROPERTIES_CHANGED = BASE_MASTER + 5;
static final int EVENT_UPSTREAM_LOST = BASE_MASTER + 6;
- // This indicates what a timeout event relates to. A state that
- // sends itself a delayed timeout event and handles incoming timeout events
- // should inc this when it is entered and whenever it sends a new timeout event.
- // We do not flush the old ones.
- private int mSequenceNumber;
-
private State mInitialState;
private State mTetherModeAliveState;
@@ -1535,7 +1129,19 @@
private State mStopTetheringErrorState;
private State mSetDnsForwardersErrorState;
- private ArrayList<TetherInterfaceSM> mNotifyList;
+ // This list is a little subtle. It contains all the interfaces that currently are
+ // requesting tethering, regardless of whether these interfaces are still members of
+ // mTetherStates. This allows us to maintain the following predicates:
+ //
+ // 1) mTetherStates contains the set of all currently existing, tetherable, link state up
+ // interfaces.
+ // 2) mNotifyList contains all state machines that may have outstanding tethering state
+ // that needs to be torn down.
+ //
+ // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList
+ // so that the garbage collector does not clean up the state machine before it has a chance
+ // to tear itself down.
+ private ArrayList<TetherInterfaceStateMachine> mNotifyList;
private int mMobileApnReserved = ConnectivityManager.TYPE_NONE;
private NetworkCallback mMobileUpstreamCallback;
@@ -1562,7 +1168,7 @@
mSetDnsForwardersErrorState = new SetDnsForwardersErrorState();
addState(mSetDnsForwardersErrorState);
- mNotifyList = new ArrayList<TetherInterfaceSM>();
+ mNotifyList = new ArrayList<>();
setInitialState(mInitialState);
}
@@ -1777,8 +1383,8 @@
protected void notifyTetheredOfNewUpstreamIface(String ifaceName) {
if (DBG) Log.d(TAG, "Notifying tethered with upstream=" + ifaceName);
mCurrentUpstreamIface = ifaceName;
- for (TetherInterfaceSM sm : mNotifyList) {
- sm.sendMessage(TetherInterfaceSM.CMD_TETHER_CONNECTION_CHANGED,
+ for (TetherInterfaceStateMachine sm : mNotifyList) {
+ sm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
ifaceName);
}
}
@@ -1845,20 +1451,16 @@
config_mobile_hotspot_provision_app_no_ui).isEmpty() == false) {
ArrayList<Integer> tethered = new ArrayList<Integer>();
synchronized (mPublicSync) {
- Set ifaces = mIfaces.keySet();
- for (Object iface : ifaces) {
- TetherInterfaceSM sm = mIfaces.get(iface);
- if (sm != null && sm.isTethered()) {
- if (isUsb((String)iface)) {
- tethered.add(new Integer(
- ConnectivityManager.TETHERING_USB));
- } else if (isWifi((String)iface)) {
- tethered.add(new Integer(
- ConnectivityManager.TETHERING_WIFI));
- } else if (isBluetooth((String)iface)) {
- tethered.add(new Integer(
- ConnectivityManager.TETHERING_BLUETOOTH));
- }
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.mLastState !=
+ IControlsTethering.STATE_TETHERED) {
+ continue; // Skip interfaces that aren't tethered.
+ }
+ String iface = mTetherStates.keyAt(i);
+ int interfaceType = ifaceNameToType(iface);
+ if (interfaceType != ConnectivityManager.TETHERING_INVALID) {
+ tethered.add(new Integer(interfaceType));
}
}
}
@@ -1885,26 +1487,22 @@
class InitialState extends TetherMasterUtilState {
@Override
- public void enter() {
- }
- @Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED:
- TetherInterfaceSM who = (TetherInterfaceSM)message.obj;
+ TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
- mNotifyList.add(who);
+ if (mNotifyList.indexOf(who) < 0) {
+ mNotifyList.add(who);
+ }
transitionTo(mTetherModeAliveState);
break;
case CMD_TETHER_MODE_UNREQUESTED:
- who = (TetherInterfaceSM)message.obj;
+ who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
- int index = mNotifyList.indexOf(who);
- if (index != -1) {
- mNotifyList.remove(who);
- }
+ mNotifyList.remove(who);
break;
default:
retValue = false;
@@ -1941,26 +1539,28 @@
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED:
- TetherInterfaceSM who = (TetherInterfaceSM)message.obj;
+ TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
- mNotifyList.add(who);
- who.sendMessage(TetherInterfaceSM.CMD_TETHER_CONNECTION_CHANGED,
+ if (mNotifyList.indexOf(who) < 0) {
+ mNotifyList.add(who);
+ }
+ who.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
mCurrentUpstreamIface);
break;
case CMD_TETHER_MODE_UNREQUESTED:
- who = (TetherInterfaceSM)message.obj;
+ who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
- int index = mNotifyList.indexOf(who);
- if (index != -1) {
+ if (mNotifyList.remove(who)) {
if (DBG) Log.d(TAG, "TetherModeAlive removing notifyee " + who);
- mNotifyList.remove(index);
if (mNotifyList.isEmpty()) {
turnOffMasterTetherSettings(); // transitions appropriately
} else {
if (DBG) {
Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
" live requests:");
- for (Object o : mNotifyList) Log.d(TAG, " " + o);
+ for (TetherInterfaceStateMachine o : mNotifyList) {
+ Log.d(TAG, " " + o);
+ }
}
}
} else {
@@ -2010,7 +1610,7 @@
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED:
- TetherInterfaceSM who = (TetherInterfaceSM)message.obj;
+ TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
who.sendMessage(mErrorNotification);
break;
default:
@@ -2020,8 +1620,7 @@
}
void notify(int msgType) {
mErrorNotification = msgType;
- for (Object o : mNotifyList) {
- TetherInterfaceSM sm = (TetherInterfaceSM)o;
+ for (TetherInterfaceStateMachine sm : mNotifyList) {
sm.sendMessage(msgType);
}
}
@@ -2031,7 +1630,7 @@
@Override
public void enter() {
Log.e(TAG, "Error in setIpForwardingEnabled");
- notify(TetherInterfaceSM.CMD_IP_FORWARDING_ENABLE_ERROR);
+ notify(TetherInterfaceStateMachine.CMD_IP_FORWARDING_ENABLE_ERROR);
}
}
@@ -2039,7 +1638,7 @@
@Override
public void enter() {
Log.e(TAG, "Error in setIpForwardingDisabled");
- notify(TetherInterfaceSM.CMD_IP_FORWARDING_DISABLE_ERROR);
+ notify(TetherInterfaceStateMachine.CMD_IP_FORWARDING_DISABLE_ERROR);
}
}
@@ -2047,7 +1646,7 @@
@Override
public void enter() {
Log.e(TAG, "Error in startTethering");
- notify(TetherInterfaceSM.CMD_START_TETHERING_ERROR);
+ notify(TetherInterfaceStateMachine.CMD_START_TETHERING_ERROR);
try {
mNMService.setIpForwardingEnabled(false);
} catch (Exception e) {}
@@ -2058,7 +1657,7 @@
@Override
public void enter() {
Log.e(TAG, "Error in stopTethering");
- notify(TetherInterfaceSM.CMD_STOP_TETHERING_ERROR);
+ notify(TetherInterfaceStateMachine.CMD_STOP_TETHERING_ERROR);
try {
mNMService.setIpForwardingEnabled(false);
} catch (Exception e) {}
@@ -2069,7 +1668,7 @@
@Override
public void enter() {
Log.e(TAG, "Error in setDnsForwarders");
- notify(TetherInterfaceSM.CMD_SET_DNS_FORWARDERS_ERROR);
+ notify(TetherInterfaceStateMachine.CMD_SET_DNS_FORWARDERS_ERROR);
try {
mNMService.stopTethering();
} catch (Exception e) {}
@@ -2080,9 +1679,11 @@
}
}
+ @Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ // Binder.java closes the resource for us.
+ @SuppressWarnings("resource")
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
-
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump ConnectivityService.Tether " +
@@ -2102,12 +1703,67 @@
pw.println("Tether state:");
pw.increaseIndent();
- for (Object o : mIfaces.values()) {
- pw.println(o);
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final String iface = mTetherStates.keyAt(i);
+ final TetherState tetherState = mTetherStates.valueAt(i);
+ pw.print(iface + " - ");
+
+ switch (tetherState.mLastState) {
+ case IControlsTethering.STATE_UNAVAILABLE:
+ pw.print("UnavailableState");
+ break;
+ case IControlsTethering.STATE_AVAILABLE:
+ pw.print("AvailableState");
+ break;
+ case IControlsTethering.STATE_TETHERED:
+ pw.print("TetheredState");
+ break;
+ default:
+ pw.print("UnknownState");
+ break;
+ }
+ pw.println(" - lastError = " + tetherState.mLastError);
}
pw.decreaseIndent();
}
pw.decreaseIndent();
- return;
+ }
+
+ @Override
+ public void notifyInterfaceStateChange(String iface, TetherInterfaceStateMachine who,
+ int state, int error) {
+ synchronized (mPublicSync) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.mStateMachine.equals(who)) {
+ tetherState.mLastState = state;
+ tetherState.mLastError = error;
+ } else {
+ if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+ }
+ }
+
+ if (DBG) {
+ Log.d(TAG, "iface " + iface + " notified that it was in state " + state +
+ " with error " + error);
+ }
+
+ switch (state) {
+ case IControlsTethering.STATE_UNAVAILABLE:
+ case IControlsTethering.STATE_AVAILABLE:
+ mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who);
+ break;
+ case IControlsTethering.STATE_TETHERED:
+ mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, who);
+ break;
+ }
+ sendTetherStateChangedBroadcast();
+ }
+
+ private void trackNewTetherableInterface(String iface, int interfaceType) {
+ TetherState tetherState;
+ tetherState = new TetherState(new TetherInterfaceStateMachine(iface, mLooper,
+ interfaceType, mNMService, mStatsService, this));
+ mTetherStates.put(iface, tetherState);
+ tetherState.mStateMachine.start();
}
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
new file mode 100644
index 0000000..449b8a8
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
@@ -0,0 +1,39 @@
+/*
+ * 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.connectivity.tethering;
+
+/**
+ * @hide
+ *
+ * Interface with methods necessary to notify that a given interface is ready for tethering.
+ */
+public interface IControlsTethering {
+ public final int STATE_UNAVAILABLE = 0;
+ public final int STATE_AVAILABLE = 1;
+ public final int STATE_TETHERED = 2;
+
+ /**
+ * Notify that |who| has changed its tethering state. This may be called from any thread.
+ *
+ * @param iface a network interface (e.g. "wlan0")
+ * @param who corresponding instance of a TetherInterfaceStateMachine
+ * @param state one of IControlsTethering.STATE_*
+ * @param lastError one of ConnectivityManager.TETHER_ERROR_*
+ */
+ void notifyInterfaceStateChange(String iface, TetherInterfaceStateMachine who,
+ int state, int lastError);
+}
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
new file mode 100644
index 0000000..aebeb69
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -0,0 +1,322 @@
+/*
+ * 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.connectivity.tethering;
+
+import android.net.ConnectivityManager;
+import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.net.InetAddress;
+
+/**
+ * @hide
+ *
+ * Tracks the eligibility of a given network interface for tethering.
+ */
+public class TetherInterfaceStateMachine extends StateMachine {
+ private static final String USB_NEAR_IFACE_ADDR = "192.168.42.129";
+ private static final int USB_PREFIX_LENGTH = 24;
+ private static final String WIFI_HOST_IFACE_ADDR = "192.168.43.1";
+ private static final int WIFI_HOST_IFACE_PREFIX_LENGTH = 24;
+
+ private final static String TAG = "TetherInterfaceSM";
+ private final static boolean DBG = false;
+ private final static boolean VDBG = false;
+ private static final Class[] messageClasses = {
+ TetherInterfaceStateMachine.class
+ };
+ private static final SparseArray<String> sMagicDecoderRing =
+ MessageUtils.findMessageNames(messageClasses);
+
+ private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100;
+ // request from the user that it wants to tether
+ public static final int CMD_TETHER_REQUESTED = BASE_IFACE + 2;
+ // request from the user that it wants to untether
+ public static final int CMD_TETHER_UNREQUESTED = BASE_IFACE + 3;
+ // notification that this interface is down
+ public static final int CMD_INTERFACE_DOWN = BASE_IFACE + 4;
+ // notification from the master SM that it had trouble enabling IP Forwarding
+ public static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IFACE + 7;
+ // notification from the master SM that it had trouble disabling IP Forwarding
+ public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IFACE + 8;
+ // notification from the master SM that it had trouble starting tethering
+ public static final int CMD_START_TETHERING_ERROR = BASE_IFACE + 9;
+ // notification from the master SM that it had trouble stopping tethering
+ public static final int CMD_STOP_TETHERING_ERROR = BASE_IFACE + 10;
+ // notification from the master SM that it had trouble setting the DNS forwarders
+ public static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IFACE + 11;
+ // the upstream connection has changed
+ public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IFACE + 12;
+
+ private final State mInitialState;
+ private final State mTetheredState;
+ private final State mUnavailableState;
+
+ private final INetworkManagementService mNMService;
+ private final INetworkStatsService mStatsService;
+ private final IControlsTethering mTetherController;
+
+ private final String mIfaceName;
+ private final int mInterfaceType;
+
+ private int mLastError;
+ private String mMyUpstreamIfaceName; // may change over time
+
+ public TetherInterfaceStateMachine(String ifaceName, Looper looper, int interfaceType,
+ INetworkManagementService nMService, INetworkStatsService statsService,
+ IControlsTethering tetherController) {
+ super(ifaceName, looper);
+ mNMService = nMService;
+ mStatsService = statsService;
+ mTetherController = tetherController;
+ mIfaceName = ifaceName;
+ mInterfaceType = interfaceType;
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+
+ mInitialState = new InitialState();
+ addState(mInitialState);
+ mTetheredState = new TetheredState();
+ addState(mTetheredState);
+ mUnavailableState = new UnavailableState();
+ addState(mUnavailableState);
+
+ setInitialState(mInitialState);
+ }
+
+ // configured when we start tethering and unconfig'd on error or conclusion
+ private boolean configureIfaceIp(boolean enabled) {
+ if (VDBG) Log.d(TAG, "configureIfaceIp(" + enabled + ")");
+
+ String ipAsString = null;
+ int prefixLen = 0;
+ if (mInterfaceType == ConnectivityManager.TETHERING_USB) {
+ ipAsString = USB_NEAR_IFACE_ADDR;
+ prefixLen = USB_PREFIX_LENGTH;
+ } else if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
+ ipAsString = WIFI_HOST_IFACE_ADDR;
+ prefixLen = WIFI_HOST_IFACE_PREFIX_LENGTH;
+ } else {
+ // Nothing to do, BT does this elsewhere.
+ return true;
+ }
+
+ InterfaceConfiguration ifcg = null;
+ try {
+ ifcg = mNMService.getInterfaceConfig(mIfaceName);
+ if (ifcg != null) {
+ InetAddress addr = NetworkUtils.numericToInetAddress(ipAsString);
+ ifcg.setLinkAddress(new LinkAddress(addr, prefixLen));
+ if (enabled) {
+ ifcg.setInterfaceUp();
+ } else {
+ ifcg.setInterfaceDown();
+ }
+ ifcg.clearFlag("running");
+ mNMService.setInterfaceConfig(mIfaceName, ifcg);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error configuring interface " + mIfaceName, e);
+ return false;
+ }
+
+ return true;
+ }
+
+ private void maybeLogMessage(State state, int what) {
+ if (DBG) {
+ Log.d(TAG, state.getName() + " got " +
+ sMagicDecoderRing.get(what, Integer.toString(what)));
+ }
+ }
+
+ class InitialState extends State {
+ @Override
+ public void enter() {
+ mTetherController.notifyInterfaceStateChange(
+ mIfaceName, TetherInterfaceStateMachine.this,
+ IControlsTethering.STATE_AVAILABLE, mLastError);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ maybeLogMessage(this, message.what);
+ boolean retValue = true;
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ transitionTo(mTetheredState);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ break;
+ default:
+ retValue = false;
+ break;
+ }
+ return retValue;
+ }
+ }
+
+ class TetheredState extends State {
+ @Override
+ public void enter() {
+ if (!configureIfaceIp(true)) {
+ mLastError = ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR;
+ transitionTo(mInitialState);
+ return;
+ }
+
+ try {
+ mNMService.tetherInterface(mIfaceName);
+ } catch (Exception e) {
+ Log.e(TAG, "Error Tethering: " + e.toString());
+ mLastError = ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+ transitionTo(mInitialState);
+ return;
+ }
+ if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
+ mTetherController.notifyInterfaceStateChange(
+ mIfaceName, TetherInterfaceStateMachine.this,
+ IControlsTethering.STATE_TETHERED, mLastError);
+ }
+
+ @Override
+ public void exit() {
+ // Note that at this point, we're leaving the tethered state. We can fail any
+ // of these operations, but it doesn't really change that we have to try them
+ // all in sequence.
+ cleanupUpstream();
+
+ try {
+ mNMService.untetherInterface(mIfaceName);
+ } catch (Exception ee) {
+ mLastError = ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+ Log.e(TAG, "Failed to untether interface: " + ee.toString());
+ }
+
+ configureIfaceIp(false);
+ }
+
+ private void cleanupUpstream() {
+ if (mMyUpstreamIfaceName != null) {
+ // note that we don't care about errors here.
+ // sometimes interfaces are gone before we get
+ // to remove their rules, which generates errors.
+ // just do the best we can.
+ try {
+ // about to tear down NAT; gather remaining statistics
+ mStatsService.forceUpdate();
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
+ }
+ try {
+ mNMService.stopInterfaceForwarding(mIfaceName, mMyUpstreamIfaceName);
+ } catch (Exception e) {
+ if (VDBG) Log.e(
+ TAG, "Exception in removeInterfaceForward: " + e.toString());
+ }
+ try {
+ mNMService.disableNat(mIfaceName, mMyUpstreamIfaceName);
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
+ }
+ mMyUpstreamIfaceName = null;
+ }
+ return;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ maybeLogMessage(this, message.what);
+ boolean retValue = true;
+ switch (message.what) {
+ case CMD_TETHER_UNREQUESTED:
+ transitionTo(mInitialState);
+ if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ String newUpstreamIfaceName = (String)(message.obj);
+ if ((mMyUpstreamIfaceName == null && newUpstreamIfaceName == null) ||
+ (mMyUpstreamIfaceName != null &&
+ mMyUpstreamIfaceName.equals(newUpstreamIfaceName))) {
+ if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
+ break;
+ }
+ cleanupUpstream();
+ if (newUpstreamIfaceName != null) {
+ try {
+ mNMService.enableNat(mIfaceName, newUpstreamIfaceName);
+ mNMService.startInterfaceForwarding(mIfaceName,
+ newUpstreamIfaceName);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception enabling Nat: " + e.toString());
+ mLastError = ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
+ transitionTo(mInitialState);
+ return true;
+ }
+ }
+ mMyUpstreamIfaceName = newUpstreamIfaceName;
+ break;
+ case CMD_IP_FORWARDING_ENABLE_ERROR:
+ case CMD_IP_FORWARDING_DISABLE_ERROR:
+ case CMD_START_TETHERING_ERROR:
+ case CMD_STOP_TETHERING_ERROR:
+ case CMD_SET_DNS_FORWARDERS_ERROR:
+ mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+ transitionTo(mInitialState);
+ break;
+ default:
+ retValue = false;
+ break;
+ }
+ return retValue;
+ }
+ }
+
+ /**
+ * This state is terminal for the per interface state machine. At this
+ * point, the master state machine should have removed this interface
+ * specific state machine from its list of possible recipients of
+ * tethering requests. The state machine itself will hang around until
+ * the garbage collector finds it.
+ */
+ class UnavailableState extends State {
+ @Override
+ public void enter() {
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ mTetherController.notifyInterfaceStateChange(
+ mIfaceName, TetherInterfaceStateMachine.this,
+ IControlsTethering.STATE_UNAVAILABLE, mLastError);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 1f6616e..a783fa2 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -20,6 +20,7 @@
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import android.Manifest;
@@ -32,6 +33,8 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.hardware.input.InputManagerInternal;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
@@ -111,11 +114,16 @@
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
+ writePulseGestureEnabled();
synchronized (mLock) {
stopDreamLocked(false /*immediate*/);
}
}
}, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.DOZE_ENABLED), false,
+ mDozeEnabledObserver, UserHandle.USER_ALL);
+ writePulseGestureEnabled();
}
}
@@ -414,6 +422,12 @@
}
}
+ private void writePulseGestureEnabled() {
+ ComponentName name = getDozeComponent();
+ boolean dozeEnabled = validateDream(name);
+ LocalServices.getService(InputManagerInternal.class).setPulseGestureEnabled(dozeEnabled);
+ }
+
private static String componentsToString(ComponentName[] componentNames) {
StringBuilder names = new StringBuilder();
if (componentNames != null) {
@@ -450,6 +464,13 @@
}
};
+ private final ContentObserver mDozeEnabledObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ writePulseGestureEnabled();
+ }
+ };
+
/**
* Handler for asynchronous operations performed by the dream manager.
* Ensures operations to {@link DreamController} are single-threaded.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index aa1d73f..74095ac 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -18,7 +18,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Build;
import android.os.LocaleList;
+import android.util.Log;
import android.view.Display;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.os.SomeArgs;
@@ -98,8 +100,11 @@
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -109,6 +114,7 @@
import java.util.List;
import java.util.Locale;
+import libcore.io.IoUtils;
import libcore.io.Streams;
import libcore.util.Objects;
@@ -136,6 +142,8 @@
private final Context mContext;
private final InputManagerHandler mHandler;
+ private final File mDoubleTouchGestureEnableFile;
+
private WindowManagerCallbacks mWindowManagerCallbacks;
private WiredAccessoryCallbacks mWiredAccessoryCallbacks;
private boolean mSystemReady;
@@ -301,6 +309,11 @@
+ mUseDevInputEventForAudioJack);
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
+ String doubleTouchGestureEnablePath = context.getResources().getString(
+ R.string.config_doubleTouchGestureEnableFile);
+ mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null :
+ new File(doubleTouchGestureEnablePath);
+
LocalServices.addService(InputManagerInternal.class, new LocalService());
}
@@ -2279,5 +2292,20 @@
public void toggleCapsLock(int deviceId) {
nativeToggleCapsLock(mPtr, deviceId);
}
+
+ @Override
+ public void setPulseGestureEnabled(boolean enabled) {
+ if (mDoubleTouchGestureEnableFile != null) {
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(mDoubleTouchGestureEnableFile);
+ writer.write(enabled ? "1" : "0");
+ } catch (IOException e) {
+ Log.wtf(TAG, "Unable to setPulseGestureEnabled", e);
+ } finally {
+ IoUtils.closeQuietly(writer);
+ }
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 0c5d293..f784b54 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -89,6 +89,7 @@
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.Manifest;
+import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
@@ -162,6 +163,7 @@
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.ArrayUtils;
@@ -187,6 +189,8 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -199,6 +203,26 @@
* Derives active rules by combining a given policy with other system status,
* and delivers to listeners, such as {@link ConnectivityManager}, for
* enforcement.
+ *
+ * <p>
+ * This class uses 2-3 locks to synchronize state:
+ * <ul>
+ * <li>{@code mUidRulesFirstLock}: used to guard state related to individual UIDs (such as firewall
+ * rules).
+ * <li>{@code mNetworkPoliciesSecondLock}: used to guard state related to network interfaces (such
+ * as network policies).
+ * <li>{@code allLocks}: not a "real" lock, but an indication (through @GuardedBy) that all locks
+ * must be held.
+ * </ul>
+ *
+ * <p>
+ * As such, methods that require synchronization have the following prefixes:
+ * <ul>
+ * <li>{@code UL()}: require the "UID" lock ({@code mUidRulesFirstLock}).
+ * <li>{@code NL()}: require the "Network" lock ({@code mNetworkPoliciesSecondLock}).
+ * <li>{@code AL()}: require all locks, which must be obtained in order ({@code mUidRulesFirstLock}
+ * first, then {@code mNetworkPoliciesSecondLock}, then {@code mYetAnotherGuardThirdLock}, etc..
+ * </ul>
*/
public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
static final String TAG = "NetworkPolicy";
@@ -282,13 +306,16 @@
private PowerManagerInternal mPowerManagerInternal;
private IDeviceIdleController mDeviceIdleController;
- final Object mRulesLock = new Object();
+ // See main javadoc for instructions on how to use these locks.
+ final Object mUidRulesFirstLock = new Object();
+ final Object mNetworkPoliciesSecondLock = new Object();
- volatile boolean mSystemReady;
- volatile boolean mScreenOn;
- volatile boolean mRestrictBackground;
- volatile boolean mRestrictPower;
- volatile boolean mDeviceIdleMode;
+ @GuardedBy("allLocks") volatile boolean mSystemReady;
+
+ @GuardedBy("mUidRulesFirstLock") volatile boolean mScreenOn;
+ @GuardedBy("mUidRulesFirstLock") volatile boolean mRestrictBackground;
+ @GuardedBy("mUidRulesFirstLock") volatile boolean mRestrictPower;
+ @GuardedBy("mUidRulesFirstLock") volatile boolean mDeviceIdleMode;
private final boolean mSuppressDefaultPolicy;
@@ -298,15 +325,19 @@
final ArrayMap<NetworkPolicy, String[]> mNetworkRules = new ArrayMap<>();
/** Defined UID policies. */
- final SparseIntArray mUidPolicy = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidPolicy = new SparseIntArray();
/** Currently derived rules for each UID. */
- final SparseIntArray mUidRules = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidRules = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallStandbyRules = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallDozableRules = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallPowerSaveRules = new SparseIntArray();
/** Set of states for the child firewall chains. True if the chain is active. */
+ @GuardedBy("mUidRulesFirstLock")
final SparseBooleanArray mFirewallChainStates = new SparseBooleanArray();
/**
@@ -314,6 +345,7 @@
* in power save mode, except device idle (doze) still applies.
* TODO: An int array might be sufficient
*/
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mPowerSaveWhitelistExceptIdleAppIds = new SparseBooleanArray();
/**
@@ -321,18 +353,22 @@
* in power save mode.
* TODO: An int array might be sufficient
*/
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mPowerSaveWhitelistAppIds = new SparseBooleanArray();
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mPowerSaveTempWhitelistAppIds = new SparseBooleanArray();
/**
* UIDs that have been white-listed to avoid restricted background.
*/
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mRestrictBackgroundWhitelistUids = new SparseBooleanArray();
/**
* UIDs that have been initially white-listed by system to avoid restricted background.
*/
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mDefaultRestrictBackgroundWhitelistUids =
new SparseBooleanArray();
@@ -340,18 +376,23 @@
* UIDs that have been initially white-listed by system to avoid restricted background,
* but later revoked by user.
*/
+ @GuardedBy("mUidRulesFirstLock")
private final SparseBooleanArray mRestrictBackgroundWhitelistRevokedUids =
new SparseBooleanArray();
/** Set of ifaces that are metered. */
+ @GuardedBy("mNetworkPoliciesSecondLock")
private ArraySet<String> mMeteredIfaces = new ArraySet<>();
/** Set of over-limit templates that have been notified. */
+ @GuardedBy("mNetworkPoliciesSecondLock")
private final ArraySet<NetworkTemplate> mOverLimitNotified = new ArraySet<>();
/** Set of currently active {@link Notification} tags. */
+ @GuardedBy("mNetworkPoliciesSecondLock")
private final ArraySet<String> mActiveNotifs = new ArraySet<String>();
/** Foreground at UID granularity. */
+ @GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidState = new SparseIntArray();
/** Higher priority listener before general event dispatch */
@@ -362,6 +403,7 @@
final Handler mHandler;
+ @GuardedBy("allLocks")
private final AtomicFile mPolicyFile;
private final AppOpsManager mAppOps;
@@ -426,7 +468,7 @@
mNotifManager = checkNotNull(notifManager, "missing INotificationManager");
}
- void updatePowerSaveWhitelistLocked() {
+ void updatePowerSaveWhitelistUL() {
try {
int[] whitelist = mDeviceIdleController.getAppIdWhitelistExceptIdle();
mPowerSaveWhitelistExceptIdleAppIds.clear();
@@ -452,19 +494,19 @@
*
* @return whether any uid has been added to {@link #mRestrictBackgroundWhitelistUids}.
*/
- boolean addDefaultRestrictBackgroundWhitelistUidsLocked() {
+ boolean addDefaultRestrictBackgroundWhitelistUidsUL() {
final List<UserInfo> users = mUserManager.getUsers();
final int numberUsers = users.size();
boolean changed = false;
for (int i = 0; i < numberUsers; i++) {
final UserInfo user = users.get(i);
- changed = addDefaultRestrictBackgroundWhitelistUidsLocked(user.id) || changed;
+ changed = addDefaultRestrictBackgroundWhitelistUidsUL(user.id) || changed;
}
return changed;
}
- private boolean addDefaultRestrictBackgroundWhitelistUidsLocked(int userId) {
+ private boolean addDefaultRestrictBackgroundWhitelistUidsUL(int userId) {
final SystemConfig sysConfig = SystemConfig.getInstance();
final PackageManager pm = mContext.getPackageManager();
final ArraySet<String> allowDataUsage = sysConfig.getAllowInDataUsageSave();
@@ -502,7 +544,7 @@
return changed;
}
- void updatePowerSaveTempWhitelistLocked() {
+ void updatePowerSaveTempWhitelistUL() {
try {
// Clear the states of the current whitelist
final int N = mPowerSaveTempWhitelistAppIds.size();
@@ -523,7 +565,7 @@
/**
* Remove unnecessary entries in the temp whitelist
*/
- void purgePowerSaveTempWhitelistLocked() {
+ void purgePowerSaveTempWhitelistUL() {
final int N = mPowerSaveTempWhitelistAppIds.size();
for (int i = N - 1; i >= 0; i--) {
if (mPowerSaveTempWhitelistAppIds.valueAt(i) == false) {
@@ -542,36 +584,38 @@
mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
- synchronized (mRulesLock) {
- updatePowerSaveWhitelistLocked();
- mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
- mPowerManagerInternal.registerLowPowerModeObserver(
- new PowerManagerInternal.LowPowerModeListener() {
- @Override
- public void onLowPowerModeChanged(boolean enabled) {
- if (LOGD) Slog.d(TAG, "onLowPowerModeChanged(" + enabled + ")");
- synchronized (mRulesLock) {
- if (mRestrictPower != enabled) {
- mRestrictPower = enabled;
- updateRulesForRestrictPowerLocked();
- updateRulesForGlobalChangeLocked(true);
+ synchronized (mUidRulesFirstLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ updatePowerSaveWhitelistUL();
+ mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
+ mPowerManagerInternal.registerLowPowerModeObserver(
+ new PowerManagerInternal.LowPowerModeListener() {
+ @Override
+ public void onLowPowerModeChanged(boolean enabled) {
+ if (LOGD) Slog.d(TAG, "onLowPowerModeChanged(" + enabled + ")");
+ synchronized (mUidRulesFirstLock) {
+ if (mRestrictPower != enabled) {
+ mRestrictPower = enabled;
+ updateRulesForRestrictPowerUL();
+ }
}
}
+ });
+ mRestrictPower = mPowerManagerInternal.getLowPowerModeEnabled();
+
+ mSystemReady = true;
+
+ // read policy from disk
+ readPolicyAL();
+
+ if (addDefaultRestrictBackgroundWhitelistUidsUL()) {
+ writePolicyAL();
}
- });
- mRestrictPower = mPowerManagerInternal.getLowPowerModeEnabled();
- mSystemReady = true;
-
- // read policy from disk
- readPolicyLocked();
-
- if (addDefaultRestrictBackgroundWhitelistUidsLocked()) {
- writePolicyLocked();
+ setRestrictBackgroundUL(mRestrictBackground);
+ updateRulesForGlobalChangeAL(false);
+ updateNotificationsNL();
}
-
- updateRulesForGlobalChangeLocked(false);
- updateNotificationsLocked();
}
updateScreenOn();
@@ -650,14 +694,14 @@
final private IUidObserver mUidObserver = new IUidObserver.Stub() {
@Override public void onUidStateChanged(int uid, int procState) throws RemoteException {
- synchronized (mRulesLock) {
- updateUidStateLocked(uid, procState);
+ synchronized (mUidRulesFirstLock) {
+ updateUidStateUL(uid, procState);
}
}
@Override public void onUidGone(int uid) throws RemoteException {
- synchronized (mRulesLock) {
- removeUidStateLocked(uid);
+ synchronized (mUidRulesFirstLock) {
+ removeUidStateUL(uid);
}
}
@@ -672,9 +716,9 @@
@Override
public void onReceive(Context context, Intent intent) {
// on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected
- synchronized (mRulesLock) {
- updatePowerSaveWhitelistLocked();
- updateRulesForGlobalChangeLocked(false);
+ synchronized (mUidRulesFirstLock) {
+ updatePowerSaveWhitelistUL();
+ updateRulesForRestrictPowerUL();
}
}
};
@@ -682,10 +726,10 @@
final private Runnable mTempPowerSaveChangedCallback = new Runnable() {
@Override
public void run() {
- synchronized (mRulesLock) {
- updatePowerSaveTempWhitelistLocked();
- updateRulesForTempWhitelistChangeLocked();
- purgePowerSaveTempWhitelistLocked();
+ synchronized (mUidRulesFirstLock) {
+ updatePowerSaveTempWhitelistUL();
+ updateRulesForTempWhitelistChangeUL();
+ purgePowerSaveTempWhitelistUL();
}
}
};
@@ -712,8 +756,8 @@
// update rules for UID, since it might be subject to
// global background data policy
if (LOGV) Slog.v(TAG, "ACTION_PACKAGE_ADDED for uid=" + uid);
- synchronized (mRulesLock) {
- updateRestrictionRulesForUidLocked(uid);
+ synchronized (mUidRulesFirstLock) {
+ updateRestrictionRulesForUidUL(uid);
}
}
}
@@ -729,10 +773,12 @@
// remove any policy and update rules to clean up
if (LOGV) Slog.v(TAG, "ACTION_UID_REMOVED for uid=" + uid);
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
mUidPolicy.delete(uid);
- updateRestrictionRulesForUidLocked(uid);
- writePolicyLocked();
+ updateRestrictionRulesForUidUL(uid);
+ synchronized (mNetworkPoliciesSecondLock) {
+ writePolicyAL();
+ }
}
}
};
@@ -750,16 +796,18 @@
switch (action) {
case ACTION_USER_REMOVED:
case ACTION_USER_ADDED:
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
// Remove any persistable state for the given user; both cleaning up after a
// USER_REMOVED, and one last sanity check during USER_ADDED
- removeUserStateLocked(userId, true);
+ removeUserStateUL(userId, true);
if (action == ACTION_USER_ADDED) {
// Add apps that are whitelisted by default.
- addDefaultRestrictBackgroundWhitelistUidsLocked(userId);
+ addDefaultRestrictBackgroundWhitelistUidsUL(userId);
}
// Update global restrict for that user
- updateRulesForGlobalChangeLocked(true);
+ synchronized (mNetworkPoliciesSecondLock) {
+ updateRulesForGlobalChangeAL(true);
+ }
}
break;
}
@@ -777,9 +825,9 @@
// READ_NETWORK_USAGE_HISTORY permission above.
maybeRefreshTrustedTime();
- synchronized (mRulesLock) {
- updateNetworkEnabledLocked();
- updateNotificationsLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ updateNetworkEnabledNL();
+ updateNotificationsNL();
}
}
};
@@ -828,10 +876,12 @@
EXTRA_WIFI_CONFIGURATION);
if (config.SSID != null) {
final NetworkTemplate template = NetworkTemplate.buildTemplateWifi(config.SSID);
- synchronized (mRulesLock) {
- if (mNetworkPolicy.containsKey(template)) {
- mNetworkPolicy.remove(template);
- writePolicyLocked();
+ synchronized (mUidRulesFirstLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ if (mNetworkPolicy.containsKey(template)) {
+ mNetworkPolicy.remove(template);
+ writePolicyAL();
+ }
}
}
}
@@ -857,13 +907,13 @@
final boolean meteredHint = info.getMeteredHint();
final NetworkTemplate template = NetworkTemplate.buildTemplateWifi(info.getSSID());
- synchronized (mRulesLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
NetworkPolicy policy = mNetworkPolicy.get(template);
if (policy == null && meteredHint) {
// policy doesn't exist, and AP is hinting that it's
// metered: create an inferred policy.
policy = newWifiPolicy(template, meteredHint);
- addNetworkPolicyLocked(policy);
+ addNetworkPolicyNL(policy);
} else if (policy != null && policy.inferred) {
// policy exists, and was inferred: update its current
@@ -872,7 +922,7 @@
// since this is inferred for each wifi session, just update
// rules without persisting.
- updateNetworkRulesLocked();
+ updateNetworkRulesNL();
}
}
}
@@ -904,8 +954,8 @@
* Check {@link NetworkPolicy} against current {@link INetworkStatsService}
* to show visible notifications as needed.
*/
- void updateNotificationsLocked() {
- if (LOGV) Slog.v(TAG, "updateNotificationsLocked()");
+ void updateNotificationsNL() {
+ if (LOGV) Slog.v(TAG, "updateNotificationsNL()");
// keep track of previously active notifications
final ArraySet<String> beforeNotifs = new ArraySet<String>(mActiveNotifs);
@@ -931,11 +981,11 @@
enqueueNotification(policy, TYPE_LIMIT_SNOOZED, totalBytes);
} else {
enqueueNotification(policy, TYPE_LIMIT, totalBytes);
- notifyOverLimitLocked(policy.template);
+ notifyOverLimitNL(policy.template);
}
} else {
- notifyUnderLimitLocked(policy.template);
+ notifyUnderLimitNL(policy.template);
if (policy.isOverWarning(totalBytes) && policy.lastWarningSnooze < start) {
enqueueNotification(policy, TYPE_WARNING, totalBytes);
@@ -983,14 +1033,14 @@
* Notify that given {@link NetworkTemplate} is over
* {@link NetworkPolicy#limitBytes}, potentially showing dialog to user.
*/
- private void notifyOverLimitLocked(NetworkTemplate template) {
+ private void notifyOverLimitNL(NetworkTemplate template) {
if (!mOverLimitNotified.contains(template)) {
mContext.startActivity(buildNetworkOverLimitIntent(template));
mOverLimitNotified.add(template);
}
}
- private void notifyUnderLimitLocked(NetworkTemplate template) {
+ private void notifyUnderLimitNL(NetworkTemplate template) {
mOverLimitNotified.remove(template);
}
@@ -1142,12 +1192,12 @@
// permission above.
maybeRefreshTrustedTime();
- synchronized (mRulesLock) {
- ensureActiveMobilePolicyLocked();
- normalizePoliciesLocked();
- updateNetworkEnabledLocked();
- updateNetworkRulesLocked();
- updateNotificationsLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ ensureActiveMobilePolicyNL();
+ normalizePoliciesNL();
+ updateNetworkEnabledNL();
+ updateNetworkRulesNL();
+ updateNotificationsNL();
}
}
};
@@ -1156,8 +1206,8 @@
* Proactively control network data connections when they exceed
* {@link NetworkPolicy#limitBytes}.
*/
- void updateNetworkEnabledLocked() {
- if (LOGV) Slog.v(TAG, "updateNetworkEnabledLocked()");
+ void updateNetworkEnabledNL() {
+ if (LOGV) Slog.v(TAG, "updateNetworkEnabledNL()");
// TODO: reset any policy-disabled networks when any policy is removed
// completely, which is currently rare case.
@@ -1198,8 +1248,8 @@
* {@link NetworkPolicy} that need to be enforced. When matches found, set
* remaining quota based on usage cycle and historical stats.
*/
- void updateNetworkRulesLocked() {
- if (LOGV) Slog.v(TAG, "updateNetworkRulesLocked()");
+ void updateNetworkRulesNL() {
+ if (LOGV) Slog.v(TAG, "updateNetworkRulesNL()");
final NetworkState[] states;
try {
@@ -1349,8 +1399,8 @@
* Once any {@link #mNetworkPolicy} are loaded from disk, ensure that we
* have at least a default mobile policy defined.
*/
- private void ensureActiveMobilePolicyLocked() {
- if (LOGV) Slog.v(TAG, "ensureActiveMobilePolicyLocked()");
+ private void ensureActiveMobilePolicyNL() {
+ if (LOGV) Slog.v(TAG, "ensureActiveMobilePolicyNL()");
if (mSuppressDefaultPolicy) return;
final TelephonyManager tele = TelephonyManager.from(mContext);
@@ -1359,11 +1409,11 @@
final int[] subIds = sub.getActiveSubscriptionIdList();
for (int subId : subIds) {
final String subscriberId = tele.getSubscriberId(subId);
- ensureActiveMobilePolicyLocked(subscriberId);
+ ensureActiveMobilePolicyNL(subscriberId);
}
}
- private void ensureActiveMobilePolicyLocked(String subscriberId) {
+ private void ensureActiveMobilePolicyNL(String subscriberId) {
// Poke around to see if we already have a policy
final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true);
@@ -1394,11 +1444,11 @@
final NetworkTemplate template = buildTemplateMobileAll(subscriberId);
final NetworkPolicy policy = new NetworkPolicy(template, cycleDay, cycleTimezone,
warningBytes, LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER, true, true);
- addNetworkPolicyLocked(policy);
+ addNetworkPolicyNL(policy);
}
- private void readPolicyLocked() {
- if (LOGV) Slog.v(TAG, "readPolicyLocked()");
+ private void readPolicyAL() {
+ if (LOGV) Slog.v(TAG, "readPolicyAL()");
// clear any existing policy and read from disk
mNetworkPolicy.clear();
@@ -1498,7 +1548,7 @@
final int policy = readIntAttribute(in, ATTR_POLICY);
if (UserHandle.isApp(uid)) {
- setUidPolicyUncheckedLocked(uid, policy, false);
+ setUidPolicyUncheckedUL(uid, policy, false);
} else {
Slog.w(TAG, "unable to apply policy to UID " + uid + "; ignoring");
}
@@ -1510,7 +1560,7 @@
// app policy is deprecated so this is only used in pre system user split.
final int uid = UserHandle.getUid(UserHandle.USER_SYSTEM, appId);
if (UserHandle.isApp(uid)) {
- setUidPolicyUncheckedLocked(uid, policy, false);
+ setUidPolicyUncheckedUL(uid, policy, false);
} else {
Slog.w(TAG, "unable to apply policy to UID " + uid + "; ignoring");
}
@@ -1533,7 +1583,7 @@
} catch (FileNotFoundException e) {
// missing policy is okay, probably first boot
- upgradeLegacyBackgroundData();
+ upgradeLegacyBackgroundDataUL();
} catch (IOException e) {
Log.wtf(TAG, "problem reading network policy", e);
} catch (XmlPullParserException e) {
@@ -1547,7 +1597,7 @@
* Upgrade legacy background data flags, notifying listeners of one last
* change to always-true.
*/
- private void upgradeLegacyBackgroundData() {
+ private void upgradeLegacyBackgroundDataUL() {
mRestrictBackground = Settings.Secure.getInt(
mContext.getContentResolver(), Settings.Secure.BACKGROUND_DATA, 1) != 1;
@@ -1559,8 +1609,8 @@
}
}
- void writePolicyLocked() {
- if (LOGV) Slog.v(TAG, "writePolicyLocked()");
+ void writePolicyAL() {
+ if (LOGV) Slog.v(TAG, "writePolicyAL()");
FileOutputStream fos = null;
try {
@@ -1657,13 +1707,12 @@
if (!UserHandle.isApp(uid)) {
throw new IllegalArgumentException("cannot apply policy to UID " + uid);
}
-
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
final long token = Binder.clearCallingIdentity();
try {
final int oldPolicy = mUidPolicy.get(uid, POLICY_NONE);
if (oldPolicy != policy) {
- setUidPolicyUncheckedLocked(uid, oldPolicy, policy, true);
+ setUidPolicyUncheckedUL(uid, oldPolicy, policy, true);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -1679,11 +1728,11 @@
throw new IllegalArgumentException("cannot apply policy to UID " + uid);
}
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
final int oldPolicy = mUidPolicy.get(uid, POLICY_NONE);
policy |= oldPolicy;
if (oldPolicy != policy) {
- setUidPolicyUncheckedLocked(uid, oldPolicy, policy, true);
+ setUidPolicyUncheckedUL(uid, oldPolicy, policy, true);
}
}
}
@@ -1696,17 +1745,17 @@
throw new IllegalArgumentException("cannot apply policy to UID " + uid);
}
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
final int oldPolicy = mUidPolicy.get(uid, POLICY_NONE);
policy = oldPolicy & ~policy;
if (oldPolicy != policy) {
- setUidPolicyUncheckedLocked(uid, oldPolicy, policy, true);
+ setUidPolicyUncheckedUL(uid, oldPolicy, policy, true);
}
}
}
- private void setUidPolicyUncheckedLocked(int uid, int oldPolicy, int policy, boolean persist) {
- setUidPolicyUncheckedLocked(uid, policy, persist);
+ private void setUidPolicyUncheckedUL(int uid, int oldPolicy, int policy, boolean persist) {
+ setUidPolicyUncheckedUL(uid, policy, persist);
final boolean isBlacklisted = policy == POLICY_REJECT_METERED_BACKGROUND;
mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_BLACKLIST_CHANGED, uid,
@@ -1721,13 +1770,15 @@
}
}
- private void setUidPolicyUncheckedLocked(int uid, int policy, boolean persist) {
+ private void setUidPolicyUncheckedUL(int uid, int policy, boolean persist) {
mUidPolicy.put(uid, policy);
// uid policy changed, recompute rules and persist policy.
- updateRulesForDataUsageRestrictionsLocked(uid);
+ updateRulesForDataUsageRestrictionsUL(uid);
if (persist) {
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ writePolicyAL();
+ }
}
}
@@ -1735,7 +1786,7 @@
public int getUidPolicy(int uid) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
return mUidPolicy.get(uid, POLICY_NONE);
}
}
@@ -1745,7 +1796,7 @@
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
int[] uids = new int[0];
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
for (int i = 0; i < mUidPolicy.size(); i++) {
final int uid = mUidPolicy.keyAt(i);
final int uidPolicy = mUidPolicy.valueAt(i);
@@ -1761,9 +1812,9 @@
* Removes any persistable state associated with given {@link UserHandle}, persisting
* if any changes that are made.
*/
- boolean removeUserStateLocked(int userId, boolean writePolicy) {
+ boolean removeUserStateUL(int userId, boolean writePolicy) {
- if (LOGV) Slog.v(TAG, "removeUserStateLocked()");
+ if (LOGV) Slog.v(TAG, "removeUserStateUL()");
boolean changed = false;
// Remove entries from restricted background UID whitelist
@@ -1777,7 +1828,7 @@
if (wlUids.length > 0) {
for (int uid : wlUids) {
- removeRestrictBackgroundWhitelistedUidLocked(uid, false, false);
+ removeRestrictBackgroundWhitelistedUidUL(uid, false, false);
}
changed = true;
}
@@ -1806,11 +1857,11 @@
}
changed = true;
}
-
- updateRulesForGlobalChangeLocked(true);
-
- if (writePolicy && changed) {
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ updateRulesForGlobalChangeAL(true);
+ if (writePolicy && changed) {
+ writePolicyAL();
+ }
}
return changed;
}
@@ -1845,19 +1896,21 @@
final long token = Binder.clearCallingIdentity();
try {
maybeRefreshTrustedTime();
- synchronized (mRulesLock) {
- normalizePoliciesLocked(policies);
- updateNetworkEnabledLocked();
- updateNetworkRulesLocked();
- updateNotificationsLocked();
- writePolicyLocked();
+ synchronized (mUidRulesFirstLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ normalizePoliciesNL(policies);
+ updateNetworkEnabledNL();
+ updateNetworkRulesNL();
+ updateNotificationsNL();
+ writePolicyAL();
+ }
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- void addNetworkPolicyLocked(NetworkPolicy policy) {
+ void addNetworkPolicyNL(NetworkPolicy policy) {
NetworkPolicy[] policies = getNetworkPolicies(mContext.getOpPackageName());
policies = ArrayUtils.appendElement(NetworkPolicy.class, policies, policy);
setNetworkPolicies(policies);
@@ -1879,7 +1932,7 @@
}
}
- synchronized (mRulesLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
final int size = mNetworkPolicy.size();
final NetworkPolicy[] policies = new NetworkPolicy[size];
for (int i = 0; i < size; i++) {
@@ -1889,11 +1942,11 @@
}
}
- private void normalizePoliciesLocked() {
- normalizePoliciesLocked(getNetworkPolicies(mContext.getOpPackageName()));
+ private void normalizePoliciesNL() {
+ normalizePoliciesNL(getNetworkPolicies(mContext.getOpPackageName()));
}
- private void normalizePoliciesLocked(NetworkPolicy[] policies) {
+ private void normalizePoliciesNL(NetworkPolicy[] policies) {
final TelephonyManager tele = TelephonyManager.from(mContext);
final String[] merged = tele.getMergedSubscriberIds();
@@ -1927,29 +1980,31 @@
void performSnooze(NetworkTemplate template, int type) {
maybeRefreshTrustedTime();
final long currentTime = currentTimeMillis();
- synchronized (mRulesLock) {
- // find and snooze local policy that matches
- final NetworkPolicy policy = mNetworkPolicy.get(template);
- if (policy == null) {
- throw new IllegalArgumentException("unable to find policy for " + template);
- }
+ synchronized (mUidRulesFirstLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ // find and snooze local policy that matches
+ final NetworkPolicy policy = mNetworkPolicy.get(template);
+ if (policy == null) {
+ throw new IllegalArgumentException("unable to find policy for " + template);
+ }
- switch (type) {
- case TYPE_WARNING:
- policy.lastWarningSnooze = currentTime;
- break;
- case TYPE_LIMIT:
- policy.lastLimitSnooze = currentTime;
- break;
- default:
- throw new IllegalArgumentException("unexpected type");
- }
+ switch (type) {
+ case TYPE_WARNING:
+ policy.lastWarningSnooze = currentTime;
+ break;
+ case TYPE_LIMIT:
+ policy.lastLimitSnooze = currentTime;
+ break;
+ default:
+ throw new IllegalArgumentException("unexpected type");
+ }
- normalizePoliciesLocked();
- updateNetworkEnabledLocked();
- updateNetworkRulesLocked();
- updateNotificationsLocked();
- writePolicyLocked();
+ normalizePoliciesNL();
+ updateNetworkEnabledNL();
+ updateNetworkRulesNL();
+ updateNotificationsNL();
+ writePolicyAL();
+ }
}
}
@@ -1957,7 +2012,7 @@
public void onTetheringChanged(String iface, boolean tethering) {
// No need to enforce permission because setRestrictBackground() will do it.
if (LOGD) Log.d(TAG, "onTetherStateChanged(" + iface + ", " + tethering + ")");
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
if (mRestrictBackground && tethering) {
Log.d(TAG, "Tethering on (" + iface +"); disable Data Saver");
setRestrictBackground(false);
@@ -1971,13 +2026,13 @@
final long token = Binder.clearCallingIdentity();
try {
maybeRefreshTrustedTime();
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
if (restrictBackground == mRestrictBackground) {
// Ideally, UI should never allow this scenario...
Slog.w(TAG, "setRestrictBackground: already " + restrictBackground);
return;
}
- setRestrictBackgroundLocked(restrictBackground);
+ setRestrictBackgroundUL(restrictBackground);
}
} finally {
@@ -1988,26 +2043,28 @@
.sendToTarget();
}
- private void setRestrictBackgroundLocked(boolean restrictBackground) {
+ private void setRestrictBackgroundUL(boolean restrictBackground) {
final boolean oldRestrictBackground = mRestrictBackground;
mRestrictBackground = restrictBackground;
// Must whitelist foreground apps before turning data saver mode on.
// TODO: there is no need to iterate through all apps here, just those in the foreground,
// so it could call AM to get the UIDs of such apps, and iterate through them instead.
- updateRulesForRestrictBackgroundLocked();
+ updateRulesForAllAppsUL(TYPE_RESTRICT_BACKGROUND);
try {
if (!mNetworkManager.setDataSaverModeEnabled(mRestrictBackground)) {
Slog.e(TAG, "Could not change Data Saver Mode on NMS to " + mRestrictBackground);
mRestrictBackground = oldRestrictBackground;
// TODO: if it knew the foreground apps (see TODO above), it could call
- // updateRulesForRestrictBackgroundLocked() again to restore state.
+ // updateRulesForRestrictBackgroundUL() again to restore state.
return;
}
} catch (RemoteException e) {
// ignored; service lives in system_server
}
- updateNotificationsLocked();
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ updateNotificationsNL();
+ writePolicyAL();
+ }
}
@Override
@@ -2015,7 +2072,8 @@
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
final boolean oldStatus;
final boolean needFirewallRules;
- synchronized (mRulesLock) {
+ int changed;
+ synchronized (mUidRulesFirstLock) {
oldStatus = mRestrictBackgroundWhitelistUids.get(uid);
if (oldStatus) {
if (LOGD) Slog.d(TAG, "uid " + uid + " is already whitelisted");
@@ -2032,12 +2090,14 @@
}
if (needFirewallRules) {
// Only update firewall rules if necessary...
- updateRulesForDataUsageRestrictionsLocked(uid);
+ updateRulesForDataUsageRestrictionsUL(uid);
}
// ...but always persists the whitelist request.
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ writePolicyAL();
+ }
+ changed = (mRestrictBackground && !oldStatus && needFirewallRules) ? 1 : 0;
}
- int changed = (mRestrictBackground && !oldStatus && needFirewallRules) ? 1 : 0;
mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, changed,
Boolean.TRUE).sendToTarget();
}
@@ -2046,8 +2106,8 @@
public void removeRestrictBackgroundWhitelistedUid(int uid) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
final boolean changed;
- synchronized (mRulesLock) {
- changed = removeRestrictBackgroundWhitelistedUidLocked(uid, false, true);
+ synchronized (mUidRulesFirstLock) {
+ changed = removeRestrictBackgroundWhitelistedUidUL(uid, false, true);
}
mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, changed ? 1 : 0,
Boolean.FALSE).sendToTarget();
@@ -2057,7 +2117,7 @@
* Removes a uid from the restricted background whitelist, returning whether its current
* {@link ConnectivityManager.RestrictBackgroundStatus} changed.
*/
- private boolean removeRestrictBackgroundWhitelistedUidLocked(int uid, boolean uidDeleted,
+ private boolean removeRestrictBackgroundWhitelistedUidUL(int uid, boolean uidDeleted,
boolean updateNow) {
final boolean oldStatus = mRestrictBackgroundWhitelistUids.get(uid);
if (!oldStatus && !uidDeleted) {
@@ -2077,11 +2137,13 @@
}
if (needFirewallRules) {
// Only update firewall rules if necessary...
- updateRulesForDataUsageRestrictionsLocked(uid, uidDeleted);
+ updateRulesForDataUsageRestrictionsUL(uid, uidDeleted);
}
if (updateNow) {
// ...but always persists the whitelist request.
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ writePolicyAL();
+ }
}
// Status only changes if Data Saver is turned on (otherwise it is DISABLED, even if the
// app was whitelisted before).
@@ -2091,7 +2153,7 @@
@Override
public int[] getRestrictBackgroundWhitelistedUids() {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
final int size = mRestrictBackgroundWhitelistUids.size();
final int[] whitelist = new int[size];
for (int i = 0; i < size; i++) {
@@ -2110,7 +2172,7 @@
mContext.enforceCallingOrSelfPermission(ACCESS_NETWORK_STATE, TAG);
final int uid = Binder.getCallingUid();
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
// Must clear identity because getUidPolicy() is restricted to system.
final long token = Binder.clearCallingIdentity();
final int policy;
@@ -2136,7 +2198,7 @@
public boolean getRestrictBackground() {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
return mRestrictBackground;
}
}
@@ -2145,13 +2207,13 @@
public void setDeviceIdleMode(boolean enabled) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
if (mDeviceIdleMode != enabled) {
mDeviceIdleMode = enabled;
if (mSystemReady) {
// Device idle change means we need to rebuild rules for all
// known apps, so do a global refresh.
- updateRulesForGlobalChangeLocked(false);
+ updateRulesForRestrictPowerUL();
}
if (enabled) {
EventLogTags.writeDeviceIdleOnPhase("net");
@@ -2162,7 +2224,7 @@
}
}
- private NetworkPolicy findPolicyForNetworkLocked(NetworkIdentity ident) {
+ private NetworkPolicy findPolicyForNetworkNL(NetworkIdentity ident) {
for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
NetworkPolicy policy = mNetworkPolicy.valueAt(i);
if (policy.template.matches(ident)) {
@@ -2190,8 +2252,8 @@
final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
final NetworkPolicy policy;
- synchronized (mRulesLock) {
- policy = findPolicyForNetworkLocked(ident);
+ synchronized (mNetworkPoliciesSecondLock) {
+ policy = findPolicyForNetworkNL(ident);
}
if (policy == null || !policy.hasCycle()) {
@@ -2229,8 +2291,8 @@
}
final NetworkPolicy policy;
- synchronized (mRulesLock) {
- policy = findPolicyForNetworkLocked(ident);
+ synchronized (mNetworkPoliciesSecondLock) {
+ policy = findPolicyForNetworkNL(ident);
}
if (policy != null) {
@@ -2255,155 +2317,157 @@
argSet.add(arg);
}
- synchronized (mRulesLock) {
- if (argSet.contains("--unsnooze")) {
- for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
- mNetworkPolicy.valueAt(i).clearSnooze();
+ synchronized (mUidRulesFirstLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ if (argSet.contains("--unsnooze")) {
+ for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
+ mNetworkPolicy.valueAt(i).clearSnooze();
+ }
+
+ normalizePoliciesNL();
+ updateNetworkEnabledNL();
+ updateNetworkRulesNL();
+ updateNotificationsNL();
+ writePolicyAL();
+
+ fout.println("Cleared snooze timestamps");
+ return;
}
- normalizePoliciesLocked();
- updateNetworkEnabledLocked();
- updateNetworkRulesLocked();
- updateNotificationsLocked();
- writePolicyLocked();
-
- fout.println("Cleared snooze timestamps");
- return;
- }
-
- fout.print("System ready: "); fout.println(mSystemReady);
- fout.print("Restrict background: "); fout.println(mRestrictBackground);
- fout.print("Restrict power: "); fout.println(mRestrictPower);
- fout.print("Device idle: "); fout.println(mDeviceIdleMode);
- fout.println("Network policies:");
- fout.increaseIndent();
- for (int i = 0; i < mNetworkPolicy.size(); i++) {
- fout.println(mNetworkPolicy.valueAt(i).toString());
- }
- fout.decreaseIndent();
-
- fout.print("Metered ifaces: "); fout.println(String.valueOf(mMeteredIfaces));
-
- fout.println("Policy for UIDs:");
- fout.increaseIndent();
- int size = mUidPolicy.size();
- for (int i = 0; i < size; i++) {
- final int uid = mUidPolicy.keyAt(i);
- final int policy = mUidPolicy.valueAt(i);
- fout.print("UID=");
- fout.print(uid);
- fout.print(" policy=");
- fout.print(DebugUtils.flagsToString(NetworkPolicyManager.class, "POLICY_", policy));
- fout.println();
- }
- fout.decreaseIndent();
-
- size = mPowerSaveWhitelistExceptIdleAppIds.size();
- if (size > 0) {
- fout.println("Power save whitelist (except idle) app ids:");
+ fout.print("System ready: "); fout.println(mSystemReady);
+ fout.print("Restrict background: "); fout.println(mRestrictBackground);
+ fout.print("Restrict power: "); fout.println(mRestrictPower);
+ fout.print("Device idle: "); fout.println(mDeviceIdleMode);
+ fout.println("Network policies:");
fout.increaseIndent();
+ for (int i = 0; i < mNetworkPolicy.size(); i++) {
+ fout.println(mNetworkPolicy.valueAt(i).toString());
+ }
+ fout.decreaseIndent();
+
+ fout.print("Metered ifaces: "); fout.println(String.valueOf(mMeteredIfaces));
+
+ fout.println("Policy for UIDs:");
+ fout.increaseIndent();
+ int size = mUidPolicy.size();
for (int i = 0; i < size; i++) {
+ final int uid = mUidPolicy.keyAt(i);
+ final int policy = mUidPolicy.valueAt(i);
fout.print("UID=");
- fout.print(mPowerSaveWhitelistExceptIdleAppIds.keyAt(i));
- fout.print(": ");
- fout.print(mPowerSaveWhitelistExceptIdleAppIds.valueAt(i));
+ fout.print(uid);
+ fout.print(" policy=");
+ fout.print(DebugUtils.flagsToString(NetworkPolicyManager.class, "POLICY_", policy));
+ fout.println();
+ }
+ fout.decreaseIndent();
+
+ size = mPowerSaveWhitelistExceptIdleAppIds.size();
+ if (size > 0) {
+ fout.println("Power save whitelist (except idle) app ids:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mPowerSaveWhitelistExceptIdleAppIds.keyAt(i));
+ fout.print(": ");
+ fout.print(mPowerSaveWhitelistExceptIdleAppIds.valueAt(i));
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
+ size = mPowerSaveWhitelistAppIds.size();
+ if (size > 0) {
+ fout.println("Power save whitelist app ids:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mPowerSaveWhitelistAppIds.keyAt(i));
+ fout.print(": ");
+ fout.print(mPowerSaveWhitelistAppIds.valueAt(i));
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
+ size = mRestrictBackgroundWhitelistUids.size();
+ if (size > 0) {
+ fout.println("Restrict background whitelist uids:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mRestrictBackgroundWhitelistUids.keyAt(i));
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
+ size = mDefaultRestrictBackgroundWhitelistUids.size();
+ if (size > 0) {
+ fout.println("Default restrict background whitelist uids:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mDefaultRestrictBackgroundWhitelistUids.keyAt(i));
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
+ size = mRestrictBackgroundWhitelistRevokedUids.size();
+ if (size > 0) {
+ fout.println("Default restrict background whitelist uids revoked by users:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mRestrictBackgroundWhitelistRevokedUids.keyAt(i));
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
+ final SparseBooleanArray knownUids = new SparseBooleanArray();
+ collectKeys(mUidState, knownUids);
+ collectKeys(mUidRules, knownUids);
+
+ fout.println("Status for all known UIDs:");
+ fout.increaseIndent();
+ size = knownUids.size();
+ for (int i = 0; i < size; i++) {
+ final int uid = knownUids.keyAt(i);
+ fout.print("UID=");
+ fout.print(uid);
+
+ final int state = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+ fout.print(" state=");
+ fout.print(state);
+ if (state <= ActivityManager.PROCESS_STATE_TOP) {
+ fout.print(" (fg)");
+ } else {
+ fout.print(state <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ ? " (fg svc)" : " (bg)");
+ }
+
+ final int uidRules = mUidRules.get(uid, RULE_NONE);
+ fout.print(" rules=");
+ fout.print(uidRulesToString(uidRules));
+ fout.println();
+ }
+ fout.decreaseIndent();
+
+ fout.println("Status for just UIDs with rules:");
+ fout.increaseIndent();
+ size = mUidRules.size();
+ for (int i = 0; i < size; i++) {
+ final int uid = mUidRules.keyAt(i);
+ fout.print("UID=");
+ fout.print(uid);
+ final int uidRules = mUidRules.get(uid, RULE_NONE);
+ fout.print(" rules=");
+ fout.print(uidRulesToString(uidRules));
fout.println();
}
fout.decreaseIndent();
}
-
- size = mPowerSaveWhitelistAppIds.size();
- if (size > 0) {
- fout.println("Power save whitelist app ids:");
- fout.increaseIndent();
- for (int i = 0; i < size; i++) {
- fout.print("UID=");
- fout.print(mPowerSaveWhitelistAppIds.keyAt(i));
- fout.print(": ");
- fout.print(mPowerSaveWhitelistAppIds.valueAt(i));
- fout.println();
- }
- fout.decreaseIndent();
- }
-
- size = mRestrictBackgroundWhitelistUids.size();
- if (size > 0) {
- fout.println("Restrict background whitelist uids:");
- fout.increaseIndent();
- for (int i = 0; i < size; i++) {
- fout.print("UID=");
- fout.print(mRestrictBackgroundWhitelistUids.keyAt(i));
- fout.println();
- }
- fout.decreaseIndent();
- }
-
- size = mDefaultRestrictBackgroundWhitelistUids.size();
- if (size > 0) {
- fout.println("Default restrict background whitelist uids:");
- fout.increaseIndent();
- for (int i = 0; i < size; i++) {
- fout.print("UID=");
- fout.print(mDefaultRestrictBackgroundWhitelistUids.keyAt(i));
- fout.println();
- }
- fout.decreaseIndent();
- }
-
- size = mRestrictBackgroundWhitelistRevokedUids.size();
- if (size > 0) {
- fout.println("Default restrict background whitelist uids revoked by users:");
- fout.increaseIndent();
- for (int i = 0; i < size; i++) {
- fout.print("UID=");
- fout.print(mRestrictBackgroundWhitelistRevokedUids.keyAt(i));
- fout.println();
- }
- fout.decreaseIndent();
- }
-
- final SparseBooleanArray knownUids = new SparseBooleanArray();
- collectKeys(mUidState, knownUids);
- collectKeys(mUidRules, knownUids);
-
- fout.println("Status for all known UIDs:");
- fout.increaseIndent();
- size = knownUids.size();
- for (int i = 0; i < size; i++) {
- final int uid = knownUids.keyAt(i);
- fout.print("UID=");
- fout.print(uid);
-
- final int state = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
- fout.print(" state=");
- fout.print(state);
- if (state <= ActivityManager.PROCESS_STATE_TOP) {
- fout.print(" (fg)");
- } else {
- fout.print(state <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- ? " (fg svc)" : " (bg)");
- }
-
- final int uidRules = mUidRules.get(uid, RULE_NONE);
- fout.print(" rules=");
- fout.print(uidRulesToString(uidRules));
- fout.println();
- }
- fout.decreaseIndent();
-
- fout.println("Status for just UIDs with rules:");
- fout.increaseIndent();
- size = mUidRules.size();
- for (int i = 0; i < size; i++) {
- final int uid = mUidRules.keyAt(i);
- fout.print("UID=");
- fout.print(uid);
- final int uidRules = mUidRules.get(uid, RULE_NONE);
- fout.print(" rules=");
- fout.print(uidRulesToString(uidRules));
- fout.println();
- }
- fout.decreaseIndent();
}
}
@@ -2418,74 +2482,74 @@
public boolean isUidForeground(int uid) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- synchronized (mRulesLock) {
- return isUidForegroundLocked(uid);
+ synchronized (mUidRulesFirstLock) {
+ return isUidForegroundUL(uid);
}
}
- private boolean isUidForegroundLocked(int uid) {
- return isUidStateForegroundLocked(
+ private boolean isUidForegroundUL(int uid) {
+ return isUidStateForegroundUL(
mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY));
}
- private boolean isUidForegroundOnRestrictBackgroundLocked(int uid) {
+ private boolean isUidForegroundOnRestrictBackgroundUL(int uid) {
final int procState = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
- return isProcStateAllowedWhileOnRestrictBackgroundLocked(procState);
+ return isProcStateAllowedWhileOnRestrictBackground(procState);
}
- private boolean isUidForegroundOnRestrictPowerLocked(int uid) {
+ private boolean isUidForegroundOnRestrictPowerUL(int uid) {
final int procState = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
return isProcStateAllowedWhileIdleOrPowerSaveMode(procState);
}
- private boolean isUidStateForegroundLocked(int state) {
+ private boolean isUidStateForegroundUL(int state) {
// only really in foreground when screen is also on
return mScreenOn && state <= ActivityManager.PROCESS_STATE_TOP;
}
/**
* Process state of UID changed; if needed, will trigger
- * {@link #updateRulesForDataUsageRestrictionsLocked(int)} and
- * {@link #updateRulesForPowerRestrictionsLocked(int)}
+ * {@link #updateRulesForDataUsageRestrictionsUL(int)} and
+ * {@link #updateRulesForPowerRestrictionsUL(int)}
*/
- private void updateUidStateLocked(int uid, int uidState) {
+ private void updateUidStateUL(int uid, int uidState) {
final int oldUidState = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
if (oldUidState != uidState) {
// state changed, push updated rules
mUidState.put(uid, uidState);
- updateRestrictBackgroundRulesOnUidStatusChangedLocked(uid, oldUidState, uidState);
+ updateRestrictBackgroundRulesOnUidStatusChangedUL(uid, oldUidState, uidState);
if (isProcStateAllowedWhileIdleOrPowerSaveMode(oldUidState)
!= isProcStateAllowedWhileIdleOrPowerSaveMode(uidState) ) {
if (isUidIdle(uid)) {
- updateRuleForAppIdleLocked(uid);
+ updateRuleForAppIdleUL(uid);
}
if (mDeviceIdleMode) {
- updateRuleForDeviceIdleLocked(uid);
+ updateRuleForDeviceIdleUL(uid);
}
if (mRestrictPower) {
- updateRuleForRestrictPowerLocked(uid);
+ updateRuleForRestrictPowerUL(uid);
}
- updateRulesForPowerRestrictionsLocked(uid);
+ updateRulesForPowerRestrictionsUL(uid);
}
- updateNetworkStats(uid, isUidStateForegroundLocked(uidState));
+ updateNetworkStats(uid, isUidStateForegroundUL(uidState));
}
}
- private void removeUidStateLocked(int uid) {
+ private void removeUidStateUL(int uid) {
final int index = mUidState.indexOfKey(uid);
if (index >= 0) {
final int oldUidState = mUidState.valueAt(index);
mUidState.removeAt(index);
if (oldUidState != ActivityManager.PROCESS_STATE_CACHED_EMPTY) {
- updateRestrictBackgroundRulesOnUidStatusChangedLocked(uid, oldUidState,
+ updateRestrictBackgroundRulesOnUidStatusChangedUL(uid, oldUidState,
ActivityManager.PROCESS_STATE_CACHED_EMPTY);
if (mDeviceIdleMode) {
- updateRuleForDeviceIdleLocked(uid);
+ updateRuleForDeviceIdleUL(uid);
}
if (mRestrictPower) {
- updateRuleForRestrictPowerLocked(uid);
+ updateRuleForRestrictPowerUL(uid);
}
- updateRulesForPowerRestrictionsLocked(uid);
+ updateRulesForPowerRestrictionsUL(uid);
updateNetworkStats(uid, false);
}
}
@@ -2500,38 +2564,38 @@
}
}
- private void updateRestrictBackgroundRulesOnUidStatusChangedLocked(int uid, int oldUidState,
+ private void updateRestrictBackgroundRulesOnUidStatusChangedUL(int uid, int oldUidState,
int newUidState) {
final boolean oldForeground =
- isProcStateAllowedWhileOnRestrictBackgroundLocked(oldUidState);
+ isProcStateAllowedWhileOnRestrictBackground(oldUidState);
final boolean newForeground =
- isProcStateAllowedWhileOnRestrictBackgroundLocked(newUidState);
+ isProcStateAllowedWhileOnRestrictBackground(newUidState);
if (oldForeground != newForeground) {
- updateRulesForDataUsageRestrictionsLocked(uid);
+ updateRulesForDataUsageRestrictionsUL(uid);
}
}
private void updateScreenOn() {
- synchronized (mRulesLock) {
+ synchronized (mUidRulesFirstLock) {
try {
mScreenOn = mPowerManager.isInteractive();
} catch (RemoteException e) {
// ignored; service lives in system_server
}
- updateRulesForScreenLocked();
+ updateRulesForScreenUL();
}
}
/**
* Update rules that might be changed by {@link #mScreenOn} value.
*/
- private void updateRulesForScreenLocked() {
+ private void updateRulesForScreenUL() {
// only update rules for anyone with foreground activities
final int size = mUidState.size();
for (int i = 0; i < size; i++) {
if (mUidState.valueAt(i) <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
final int uid = mUidState.keyAt(i);
- updateRestrictionRulesForUidLocked(uid);
+ updateRestrictionRulesForUidUL(uid);
}
}
}
@@ -2540,31 +2604,31 @@
return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
}
- static boolean isProcStateAllowedWhileOnRestrictBackgroundLocked(int procState) {
+ static boolean isProcStateAllowedWhileOnRestrictBackground(int procState) {
return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
}
- void updateRulesForRestrictPowerLocked() {
- updateRulesForWhitelistedPowerSaveLocked(mRestrictPower, FIREWALL_CHAIN_POWERSAVE,
+ void updateRulesForPowerSaveUL() {
+ updateRulesForWhitelistedPowerSaveUL(mRestrictPower, FIREWALL_CHAIN_POWERSAVE,
mUidFirewallPowerSaveRules);
}
- void updateRuleForRestrictPowerLocked(int uid) {
- updateRulesForWhitelistedPowerSaveLocked(uid, mRestrictPower, FIREWALL_CHAIN_POWERSAVE);
+ void updateRuleForRestrictPowerUL(int uid) {
+ updateRulesForWhitelistedPowerSaveUL(uid, mRestrictPower, FIREWALL_CHAIN_POWERSAVE);
}
- void updateRulesForDeviceIdleLocked() {
- updateRulesForWhitelistedPowerSaveLocked(mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE,
+ void updateRulesForDeviceIdleUL() {
+ updateRulesForWhitelistedPowerSaveUL(mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE,
mUidFirewallDozableRules);
}
- void updateRuleForDeviceIdleLocked(int uid) {
- updateRulesForWhitelistedPowerSaveLocked(uid, mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE);
+ void updateRuleForDeviceIdleUL(int uid) {
+ updateRulesForWhitelistedPowerSaveUL(uid, mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE);
}
// NOTE: since both fw_dozable and fw_powersave uses the same map
// (mPowerSaveTempWhitelistAppIds) for whitelisting, we can reuse their logic in this method.
- private void updateRulesForWhitelistedPowerSaveLocked(boolean enabled, int chain,
+ private void updateRulesForWhitelistedPowerSaveUL(boolean enabled, int chain,
SparseIntArray rules) {
if (enabled) {
// Sync the whitelists before enabling the chain. We don't care about the rules if
@@ -2595,23 +2659,19 @@
setUidFirewallRules(chain, uidRules);
}
- enableFirewallChainLocked(chain, enabled);
+ enableFirewallChainUL(chain, enabled);
}
- private void updateRulesForNonMeteredNetworksLocked() {
-
- }
-
- private boolean isWhitelistedBatterySaverLocked(int uid) {
+ private boolean isWhitelistedBatterySaverUL(int uid) {
final int appId = UserHandle.getAppId(uid);
return mPowerSaveTempWhitelistAppIds.get(appId) || mPowerSaveWhitelistAppIds.get(appId);
}
// NOTE: since both fw_dozable and fw_powersave uses the same map
// (mPowerSaveTempWhitelistAppIds) for whitelisting, we can reuse their logic in this method.
- private void updateRulesForWhitelistedPowerSaveLocked(int uid, boolean enabled, int chain) {
+ private void updateRulesForWhitelistedPowerSaveUL(int uid, boolean enabled, int chain) {
if (enabled) {
- if (isWhitelistedBatterySaverLocked(uid)
+ if (isWhitelistedBatterySaverUL(uid)
|| isProcStateAllowedWhileIdleOrPowerSaveMode(mUidState.get(uid))) {
setUidFirewallRule(chain, uid, FIREWALL_RULE_ALLOW);
} else {
@@ -2620,7 +2680,7 @@
}
}
- void updateRulesForAppIdleLocked() {
+ void updateRulesForAppIdleUL() {
final SparseIntArray uidRules = mUidFirewallStandbyRules;
uidRules.clear();
@@ -2644,50 +2704,69 @@
setUidFirewallRules(FIREWALL_CHAIN_STANDBY, uidRules);
}
- void updateRuleForAppIdleLocked(int uid) {
+ void updateRuleForAppIdleUL(int uid) {
if (!isUidValidForBlacklistRules(uid)) return;
int appId = UserHandle.getAppId(uid);
if (!mPowerSaveTempWhitelistAppIds.get(appId) && isUidIdle(uid)
- && !isUidForegroundOnRestrictPowerLocked(uid)) {
+ && !isUidForegroundOnRestrictPowerUL(uid)) {
setUidFirewallRule(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DENY);
} else {
setUidFirewallRule(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DEFAULT);
}
}
- void updateRulesForAppIdleParoleLocked() {
+ void updateRulesForAppIdleParoleUL() {
boolean enableChain = !mUsageStats.isAppIdleParoleOn();
- enableFirewallChainLocked(FIREWALL_CHAIN_STANDBY, enableChain);
+ enableFirewallChainUL(FIREWALL_CHAIN_STANDBY, enableChain);
}
/**
* Update rules that might be changed by {@link #mRestrictBackground},
* {@link #mRestrictPower}, or {@link #mDeviceIdleMode} value.
*/
- private void updateRulesForGlobalChangeLocked(boolean restrictedNetworksChanged) {
+ private void updateRulesForGlobalChangeAL(boolean restrictedNetworksChanged) {
long start;
if (LOGD) start = System.currentTimeMillis();
- updateRulesForDeviceIdleLocked();
- updateRulesForAppIdleLocked();
- updateRulesForRestrictPowerLocked();
- updateRulesForRestrictBackgroundLocked();
- setRestrictBackgroundLocked(mRestrictBackground);
+ updateRulesForRestrictPowerUL();
+ updateRulesForRestrictBackgroundUL();
// If the set of restricted networks may have changed, re-evaluate those.
if (restrictedNetworksChanged) {
- normalizePoliciesLocked();
- updateNetworkRulesLocked();
+ normalizePoliciesNL();
+ updateNetworkRulesNL();
}
if (LOGD) {
final long delta = System.currentTimeMillis() - start;
- Slog.d(TAG, "updateRulesForGlobalChangeLocked(" + restrictedNetworksChanged + ") took "
+ Slog.d(TAG, "updateRulesForGlobalChangeAL(" + restrictedNetworksChanged + ") took "
+ delta + "ms");
}
}
- private void updateRulesForRestrictBackgroundLocked() {
+ private void updateRulesForRestrictPowerUL() {
+ updateRulesForDeviceIdleUL();
+ updateRulesForAppIdleUL();
+ updateRulesForPowerSaveUL();
+ updateRulesForAllAppsUL(TYPE_RESTRICT_POWER);
+ }
+
+ private void updateRulesForRestrictBackgroundUL() {
+ updateRulesForAllAppsUL(TYPE_RESTRICT_BACKGROUND);
+ }
+
+ private static final int TYPE_RESTRICT_BACKGROUND = 1;
+ private static final int TYPE_RESTRICT_POWER = 2;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, value = {
+ TYPE_RESTRICT_BACKGROUND,
+ TYPE_RESTRICT_POWER,
+ })
+ public @interface RestrictType {
+ }
+
+ // TODO: refactor / consolidate all those updateXyz methods, there are way too many of them...
+ private void updateRulesForAllAppsUL(@RestrictType int type) {
final PackageManager pm = mContext.getPackageManager();
// update rules for all installed applications
@@ -2704,25 +2783,34 @@
for (int j = 0; j < appsSize; j++) {
final ApplicationInfo app = apps.get(j);
final int uid = UserHandle.getUid(user.id, app.uid);
- updateRulesForDataUsageRestrictionsLocked(uid);
- updateRulesForPowerRestrictionsLocked(uid);
+ switch (type) {
+ case TYPE_RESTRICT_BACKGROUND:
+ updateRulesForDataUsageRestrictionsUL(uid);
+ break;
+ case TYPE_RESTRICT_POWER:
+ updateRulesForPowerRestrictionsUL(uid);
+ break;
+ default:
+ Slog.w(TAG, "Invalid type for updateRulesForAllApps: " + type);
+ }
}
}
}
- private void updateRulesForTempWhitelistChangeLocked() {
+ private void updateRulesForTempWhitelistChangeUL() {
final List<UserInfo> users = mUserManager.getUsers();
for (int i = 0; i < users.size(); i++) {
final UserInfo user = users.get(i);
for (int j = mPowerSaveTempWhitelistAppIds.size() - 1; j >= 0; j--) {
int appId = mPowerSaveTempWhitelistAppIds.keyAt(j);
int uid = UserHandle.getUid(user.id, appId);
+ updateRulesForRestrictPowerUL();
// Update external firewall rules.
- updateRuleForAppIdleLocked(uid);
- updateRuleForDeviceIdleLocked(uid);
- updateRuleForRestrictPowerLocked(uid);
+ updateRuleForAppIdleUL(uid);
+ updateRuleForDeviceIdleUL(uid);
+ updateRuleForRestrictPowerUL(uid);
// Update internal rules.
- updateRulesForPowerRestrictionsLocked(uid);
+ updateRulesForPowerRestrictionsUL(uid);
}
}
}
@@ -2786,17 +2874,17 @@
*
* <p>This method changes both the external firewall rules and the internal state.
*/
- private void updateRestrictionRulesForUidLocked(int uid) {
+ private void updateRestrictionRulesForUidUL(int uid) {
// Methods below only changes the firewall rules for the power-related modes.
- updateRuleForDeviceIdleLocked(uid);
- updateRuleForAppIdleLocked(uid);
- updateRuleForRestrictPowerLocked(uid);
+ updateRuleForDeviceIdleUL(uid);
+ updateRuleForAppIdleUL(uid);
+ updateRuleForRestrictPowerUL(uid);
// Update internal state for power-related modes.
- updateRulesForPowerRestrictionsLocked(uid);
+ updateRulesForPowerRestrictionsUL(uid);
// Update firewall and internal rules for Data Saver Mode.
- updateRulesForDataUsageRestrictionsLocked(uid);
+ updateRulesForDataUsageRestrictionsUL(uid);
}
/**
@@ -2818,7 +2906,7 @@
* {@link #setUidPolicy(int, int)} and {@link #addRestrictBackgroundWhitelistedUid(int)} /
* {@link #removeRestrictBackgroundWhitelistedUid(int)} methods (for blacklist and whitelist
* respectively): these methods set the proper internal state (blacklist / whitelist), then call
- * this ({@link #updateRulesForDataUsageRestrictionsLocked(int)}) to propagate the rules to
+ * this ({@link #updateRulesForDataUsageRestrictionsUL(int)}) to propagate the rules to
* {@link INetworkManagementService}, but this method should also be called in events (like
* Data Saver Mode flips or UID state changes) that might affect the foreground app, since the
* following rules should also be applied:
@@ -2838,15 +2926,15 @@
* <p>The {@link #mUidRules} map is used to define the transtion of states of an UID.
*
*/
- private void updateRulesForDataUsageRestrictionsLocked(int uid) {
- updateRulesForDataUsageRestrictionsLocked(uid, false);
+ private void updateRulesForDataUsageRestrictionsUL(int uid) {
+ updateRulesForDataUsageRestrictionsUL(uid, false);
}
/**
- * Overloaded version of {@link #updateRulesForDataUsageRestrictionsLocked(int)} called when an
+ * Overloaded version of {@link #updateRulesForDataUsageRestrictionsUL(int)} called when an
* app is removed - it ignores the UID validity check.
*/
- private void updateRulesForDataUsageRestrictionsLocked(int uid, boolean uidDeleted) {
+ private void updateRulesForDataUsageRestrictionsUL(int uid, boolean uidDeleted) {
if (!uidDeleted && !isUidValidForWhitelistRules(uid)) {
if (LOGD) Slog.d(TAG, "no need to update restrict data rules for uid " + uid);
return;
@@ -2854,7 +2942,7 @@
final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
final int oldUidRules = mUidRules.get(uid, RULE_NONE);
- final boolean isForeground = isUidForegroundOnRestrictBackgroundLocked(uid);
+ final boolean isForeground = isUidForegroundOnRestrictBackgroundUL(uid);
final boolean isBlacklisted = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
final boolean isWhitelisted = mRestrictBackgroundWhitelistUids.get(uid);
@@ -2878,7 +2966,7 @@
final int newUidRules = newRule | (oldUidRules & MASK_ALL_NETWORKS);
if (LOGV) {
- Log.v(TAG, "updateRuleForRestrictBackgroundLocked(" + uid + ")"
+ Log.v(TAG, "updateRuleForRestrictBackgroundUL(" + uid + ")"
+ ": isForeground=" +isForeground
+ ", isBlacklisted=" + isBlacklisted
+ ", isWhitelisted=" + isWhitelisted
@@ -2971,7 +3059,7 @@
* <p>
* <strong>NOTE: </strong>This method does not update the firewall rules on {@code netd}.
*/
- private void updateRulesForPowerRestrictionsLocked(int uid) {
+ private void updateRulesForPowerRestrictionsUL(int uid) {
if (!isUidValidForBlacklistRules(uid)) {
if (LOGD) Slog.d(TAG, "no need to update restrict power rules for uid " + uid);
return;
@@ -2981,9 +3069,9 @@
final boolean restrictMode = isIdle || mRestrictPower || mDeviceIdleMode;
final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
final int oldUidRules = mUidRules.get(uid, RULE_NONE);
- final boolean isForeground = isUidForegroundOnRestrictPowerLocked(uid);
+ final boolean isForeground = isUidForegroundOnRestrictPowerUL(uid);
- final boolean isWhitelisted = isWhitelistedBatterySaverLocked(uid);
+ final boolean isWhitelisted = isWhitelistedBatterySaverUL(uid);
final int oldRule = oldUidRules & MASK_ALL_NETWORKS;
int newRule = RULE_NONE;
@@ -3002,7 +3090,7 @@
final int newUidRules = (oldUidRules & MASK_METERED_NETWORKS) | newRule;
if (LOGV) {
- Log.v(TAG, "updateRulesForNonMeteredNetworksLocked(" + uid + ")"
+ Log.v(TAG, "updateRulesForNonMeteredNetworksUL(" + uid + ")"
+ ", isIdle: " + isIdle
+ ", mRestrictPower: " + mRestrictPower
+ ", mDeviceIdleMode: " + mDeviceIdleMode
@@ -3047,9 +3135,9 @@
final int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
if (LOGV) Log.v(TAG, "onAppIdleStateChanged(): uid=" + uid + ", idle=" + idle);
- synchronized (mRulesLock) {
- updateRuleForAppIdleLocked(uid);
- updateRulesForPowerRestrictionsLocked(uid);
+ synchronized (mUidRulesFirstLock) {
+ updateRuleForAppIdleUL(uid);
+ updateRulesForPowerRestrictionsUL(uid);
}
} catch (NameNotFoundException nnfe) {
}
@@ -3057,8 +3145,8 @@
@Override
public void onParoleStateChanged(boolean isParoleOn) {
- synchronized (mRulesLock) {
- updateRulesForAppIdleParoleLocked();
+ synchronized (mUidRulesFirstLock) {
+ updateRulesForAppIdleParoleUL();
}
}
}
@@ -3143,7 +3231,7 @@
final String iface = (String) msg.obj;
maybeRefreshTrustedTime();
- synchronized (mRulesLock) {
+ synchronized (mNetworkPoliciesSecondLock) {
if (mMeteredIfaces.contains(iface)) {
try {
// force stats update to make sure we have
@@ -3153,8 +3241,8 @@
// ignored; service lives in system_server
}
- updateNetworkEnabledLocked();
- updateNotificationsLocked();
+ updateNetworkEnabledNL();
+ updateNotificationsNL();
}
}
return true;
@@ -3354,7 +3442,7 @@
/**
* Add or remove a uid to the firewall blacklist for all network ifaces.
*/
- private void enableFirewallChainLocked(int chain, boolean enable) {
+ private void enableFirewallChainUL(int chain, boolean enable) {
if (mFirewallChainStates.indexOfKey(chain) >= 0 &&
mFirewallChainStates.get(chain) == enable) {
// All is the same, nothing to do.
@@ -3483,9 +3571,9 @@
@Override
public void onPackageRemoved(String packageName, int uid) {
if (LOGV) Slog.v(TAG, "onPackageRemoved: " + packageName + " ->" + uid);
- synchronized (mRulesLock) {
- removeRestrictBackgroundWhitelistedUidLocked(uid, true, true);
- updateRestrictionRulesForUidLocked(uid);
+ synchronized (mUidRulesFirstLock) {
+ removeRestrictBackgroundWhitelistedUidUL(uid, true, true);
+ updateRestrictionRulesForUidUL(uid);
}
}
}
@@ -3494,11 +3582,13 @@
@Override
public void resetUserState(int userId) {
- synchronized (mRulesLock) {
- boolean changed = removeUserStateLocked(userId, false);
- changed = addDefaultRestrictBackgroundWhitelistUidsLocked(userId) || changed;
+ synchronized (mUidRulesFirstLock) {
+ boolean changed = removeUserStateUL(userId, false);
+ changed = addDefaultRestrictBackgroundWhitelistUidsUL(userId) || changed;
if (changed) {
- writePolicyLocked();
+ synchronized (mNetworkPoliciesSecondLock) {
+ writePolicyAL();
+ }
}
}
}
diff --git a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
index fe6fb1f..b25ef17 100644
--- a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
+++ b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
@@ -64,11 +64,12 @@
mIntent = new Intent().setComponent(componentName);
}
- public final List<EphemeralResolveInfo> getEphemeralResolveInfoList(int hashPrefix) {
+ public final List<EphemeralResolveInfo> getEphemeralResolveInfoList(
+ int hashPrefix[], int prefixMask) {
throwIfCalledOnMainThread();
try {
return mGetEphemeralResolveInfoCaller.getEphemeralResolveInfoList(
- getRemoteInstanceLazy(), hashPrefix);
+ getRemoteInstanceLazy(), hashPrefix, prefixMask);
} catch (RemoteException re) {
} catch (TimeoutException te) {
} finally {
@@ -177,10 +178,10 @@
}
public List<EphemeralResolveInfo> getEphemeralResolveInfoList(
- IEphemeralResolver target, int hashPrefix)
+ IEphemeralResolver target, int hashPrefix[], int prefixMask)
throws RemoteException, TimeoutException {
final int sequence = onBeforeRemoteCall();
- target.getEphemeralResolveInfoList(mCallback, hashPrefix, sequence);
+ target.getEphemeralResolveInfoList(mCallback, hashPrefix, prefixMask, sequence);
return getResultTimed(sequence);
}
}
diff --git a/services/core/java/com/android/server/pm/IntentFilterVerificationState.java b/services/core/java/com/android/server/pm/IntentFilterVerificationState.java
index c6e7911..a4e9d10 100644
--- a/services/core/java/com/android/server/pm/IntentFilterVerificationState.java
+++ b/services/core/java/com/android/server/pm/IntentFilterVerificationState.java
@@ -96,7 +96,12 @@
if (i > 0) {
sb.append(" ");
}
- sb.append(mHosts.valueAt(i));
+ String host = mHosts.valueAt(i);
+ // "*.example.tld" is validated via https://example.tld
+ if (host.startsWith("*.")) {
+ host = host.substring(2);
+ }
+ sb.append(host);
}
return sb.toString();
}
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index ffe8f75..6e006f1 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -745,9 +745,6 @@
@Override
public void onShortcutChanged(@NonNull String packageName,
@UserIdInt int userId) {
- if (!ShortcutService.FEATURE_ENABLED) {
- return;
- }
postToPackageMonitorHandler(() -> onShortcutChangedInner(packageName, userId));
}
@@ -778,8 +775,7 @@
/* changedSince= */ 0, packageName, /* shortcutIds=*/ null,
/* component= */ null,
ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY
- | ShortcutQuery.FLAG_GET_PINNED
- | ShortcutQuery.FLAG_GET_DYNAMIC
+ | ShortcutQuery.FLAG_GET_ALL_KINDS
, userId);
try {
listener.onShortcutChanged(user, packageName,
diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java
index 649a27c..df91f4a 100644
--- a/services/core/java/com/android/server/pm/OtaDexoptService.java
+++ b/services/core/java/com/android/server/pm/OtaDexoptService.java
@@ -54,6 +54,7 @@
// TODO: Evaluate the need for WeakReferences here.
private List<PackageParser.Package> mDexoptPackages;
+ private int completeSize;
public OtaDexoptService(Context context, PackageManagerService packageManagerService) {
this.mContext = context;
@@ -91,6 +92,7 @@
mDexoptPackages = PackageManagerServiceUtils.getPackagesForDexopt(
mPackageManagerService.mPackages.values(), mPackageManagerService);
}
+ completeSize = mDexoptPackages.size();
}
@Override
@@ -111,6 +113,14 @@
}
@Override
+ public synchronized float getProgress() throws RemoteException {
+ if (completeSize == 0) {
+ return 1f;
+ }
+ return (completeSize - mDexoptPackages.size()) / ((float)completeSize);
+ }
+
+ @Override
public synchronized void dexoptNextPackage() throws RemoteException {
if (mDexoptPackages == null) {
throw new IllegalStateException("dexoptNextPackage() called before prepare()");
diff --git a/services/core/java/com/android/server/pm/OtaDexoptShellCommand.java b/services/core/java/com/android/server/pm/OtaDexoptShellCommand.java
index ea9cf17..e8fdfa5 100644
--- a/services/core/java/com/android/server/pm/OtaDexoptShellCommand.java
+++ b/services/core/java/com/android/server/pm/OtaDexoptShellCommand.java
@@ -46,6 +46,8 @@
return runOtaDone();
case "step":
return runOtaStep();
+ case "progress":
+ return runOtaProgress();
default:
return handleDefaultCommands(cmd);
}
@@ -81,6 +83,13 @@
return 0;
}
+ private int runOtaProgress() throws RemoteException {
+ final float progress = mInterface.getProgress();
+ final PrintWriter pw = getOutPrintWriter();
+ pw.format("%.2f", progress);
+ return 0;
+ }
+
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 12917b4..c3934a7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -125,6 +125,7 @@
import android.content.pm.ComponentInfo;
import android.content.pm.EphemeralApplicationInfo;
import android.content.pm.EphemeralResolveInfo;
+import android.content.pm.EphemeralResolveInfo.EphemeralDigest;
import android.content.pm.EphemeralResolveInfo.EphemeralResolveIntentInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IOnPermissionsChangeListener;
@@ -197,6 +198,7 @@
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.os.storage.VolumeRecord;
+import android.provider.Settings.Global;
import android.security.KeyStore;
import android.security.SystemKeyStore;
import android.system.ErrnoException;
@@ -362,13 +364,13 @@
static final boolean DEBUG_DEXOPT = false;
private static final boolean DEBUG_ABI_SELECTION = false;
- private static final boolean DEBUG_EPHEMERAL = false;
+ private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE;
private static final boolean DEBUG_TRIAGED_MISSING = false;
private static final boolean DEBUG_APP_DATA = false;
static final boolean CLEAR_RUNTIME_PERMISSIONS_ON_UPGRADE = false;
- private static final boolean DISABLE_EPHEMERAL_APPS = true;
+ private static final boolean DISABLE_EPHEMERAL_APPS = !Build.IS_DEBUGGABLE;
private static final int RADIO_UID = Process.PHONE_UID;
private static final int LOG_UID = Process.LOG_UID;
@@ -462,6 +464,9 @@
private static final String VENDOR_OVERLAY_DIR = "/vendor/overlay";
+ private static int DEFAULT_EPHEMERAL_HASH_PREFIX_MASK = 0xFFFFF000;
+ private static int DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT = 5;
+
/** Permission grant: not grant the permission. */
private static final int GRANT_DENIED = 1;
@@ -2946,17 +2951,20 @@
private @Nullable ComponentName getEphemeralResolverLPr() {
final String[] packageArray =
mContext.getResources().getStringArray(R.array.config_ephemeralResolverPackage);
- if (packageArray.length == 0) {
+ if (packageArray.length == 0 && !Build.IS_DEBUGGABLE) {
if (DEBUG_EPHEMERAL) {
Slog.d(TAG, "Ephemeral resolver NOT found; empty package list");
}
return null;
}
+ final int resolveFlags =
+ MATCH_DIRECT_BOOT_AWARE
+ | MATCH_DIRECT_BOOT_UNAWARE
+ | (!Build.IS_DEBUGGABLE ? MATCH_SYSTEM_ONLY : 0);
final Intent resolverIntent = new Intent(Intent.ACTION_RESOLVE_EPHEMERAL_PACKAGE);
final List<ResolveInfo> resolvers = queryIntentServicesInternal(resolverIntent, null,
- MATCH_SYSTEM_ONLY | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
- UserHandle.USER_SYSTEM);
+ resolveFlags, UserHandle.USER_SYSTEM);
final int N = resolvers.size();
if (N == 0) {
@@ -2975,7 +2983,7 @@
}
final String packageName = info.serviceInfo.packageName;
- if (!possiblePackages.contains(packageName)) {
+ if (!possiblePackages.contains(packageName) && !Build.IS_DEBUGGABLE) {
if (DEBUG_EPHEMERAL) {
Slog.d(TAG, "Ephemeral resolver not in allowed package list;"
+ " pkg: " + packageName + ", info:" + info);
@@ -3000,9 +3008,12 @@
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(Uri.fromFile(new File("foo.apk")), PACKAGE_MIME_TYPE);
+ final int resolveFlags =
+ MATCH_DIRECT_BOOT_AWARE
+ | MATCH_DIRECT_BOOT_UNAWARE
+ | (!Build.IS_DEBUGGABLE ? MATCH_SYSTEM_ONLY : 0);
final List<ResolveInfo> matches = queryIntentActivitiesInternal(intent, PACKAGE_MIME_TYPE,
- MATCH_SYSTEM_ONLY | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
- UserHandle.USER_SYSTEM);
+ resolveFlags, UserHandle.USER_SYSTEM);
if (matches.size() == 0) {
return null;
} else if (matches.size() == 1) {
@@ -4991,48 +5002,44 @@
private EphemeralResolveInfo getEphemeralResolveInfo(Intent intent, String resolvedType,
int userId) {
- MessageDigest digest = null;
- try {
- digest = MessageDigest.getInstance(EphemeralResolveInfo.SHA_ALGORITHM);
- } catch (NoSuchAlgorithmException e) {
- // If we can't create a digest, ignore ephemeral apps.
- return null;
- }
-
- final byte[] hostBytes = intent.getData().getHost().getBytes();
- final byte[] digestBytes = digest.digest(hostBytes);
- int shaPrefix =
- digestBytes[0] << 24
- | digestBytes[1] << 16
- | digestBytes[2] << 8
- | digestBytes[3] << 0;
+ final int ephemeralPrefixMask = Global.getInt(mContext.getContentResolver(),
+ Global.EPHEMERAL_HASH_PREFIX_MASK, DEFAULT_EPHEMERAL_HASH_PREFIX_MASK);
+ final int ephemeralPrefixCount = Global.getInt(mContext.getContentResolver(),
+ Global.EPHEMERAL_HASH_PREFIX_COUNT, DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT);
+ final EphemeralDigest digest = new EphemeralDigest(intent.getData(), ephemeralPrefixCount);
+ final int[] shaPrefix = digest.getDigestPrefix();
+ final byte[][] digestBytes = digest.getDigestBytes();
final List<EphemeralResolveInfo> ephemeralResolveInfoList =
- mEphemeralResolverConnection.getEphemeralResolveInfoList(shaPrefix);
+ mEphemeralResolverConnection.getEphemeralResolveInfoList(
+ shaPrefix, ephemeralPrefixMask);
if (ephemeralResolveInfoList == null || ephemeralResolveInfoList.size() == 0) {
// No hash prefix match; there are no ephemeral apps for this domain.
return null;
}
- for (int i = ephemeralResolveInfoList.size() - 1; i >= 0; --i) {
- EphemeralResolveInfo ephemeralApplication = ephemeralResolveInfoList.get(i);
- if (!Arrays.equals(digestBytes, ephemeralApplication.getDigestBytes())) {
- continue;
- }
- final List<IntentFilter> filters = ephemeralApplication.getFilters();
- // No filters; this should never happen.
- if (filters.isEmpty()) {
- continue;
- }
- // We have a domain match; resolve the filters to see if anything matches.
- final EphemeralIntentResolver ephemeralResolver = new EphemeralIntentResolver();
- for (int j = filters.size() - 1; j >= 0; --j) {
- final EphemeralResolveIntentInfo intentInfo =
- new EphemeralResolveIntentInfo(filters.get(j), ephemeralApplication);
- ephemeralResolver.addFilter(intentInfo);
- }
- List<EphemeralResolveInfo> matchedResolveInfoList = ephemeralResolver.queryIntent(
- intent, resolvedType, false /*defaultOnly*/, userId);
- if (!matchedResolveInfoList.isEmpty()) {
- return matchedResolveInfoList.get(0);
+
+ // Go in reverse order so we match the narrowest scope first.
+ for (int i = shaPrefix.length - 1; i >= 0 ; --i) {
+ for (EphemeralResolveInfo ephemeralApplication : ephemeralResolveInfoList) {
+ if (!Arrays.equals(digestBytes[i], ephemeralApplication.getDigestBytes())) {
+ continue;
+ }
+ final List<IntentFilter> filters = ephemeralApplication.getFilters();
+ // No filters; this should never happen.
+ if (filters.isEmpty()) {
+ continue;
+ }
+ // We have a domain match; resolve the filters to see if anything matches.
+ final EphemeralIntentResolver ephemeralResolver = new EphemeralIntentResolver();
+ for (int j = filters.size() - 1; j >= 0; --j) {
+ final EphemeralResolveIntentInfo intentInfo =
+ new EphemeralResolveIntentInfo(filters.get(j), ephemeralApplication);
+ ephemeralResolver.addFilter(intentInfo);
+ }
+ List<EphemeralResolveInfo> matchedResolveInfoList = ephemeralResolver.queryIntent(
+ intent, resolvedType, false /*defaultOnly*/, userId);
+ if (!matchedResolveInfoList.isEmpty()) {
+ return matchedResolveInfoList.get(0);
+ }
}
}
// Hash or filter mis-match; no ephemeral apps for this domain.
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index dfd6dfe..5126305 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4011,7 +4011,7 @@
file.delete();
removeCrossProfileIntentFiltersLPw(userId);
- mRuntimePermissionsPersistence.onUserRemoved(userId);
+ mRuntimePermissionsPersistence.onUserRemovedLPw(userId);
writePackageListLPr();
}
@@ -5108,7 +5108,7 @@
}
}
- private void onUserRemoved(int userId) {
+ private void onUserRemovedLPw(int userId) {
// Make sure we do not
mHandler.removeMessages(userId);
@@ -5119,6 +5119,9 @@
for (SettingBase sb : mSharedUsers.values()) {
revokeRuntimePermissionsAndClearFlags(sb, userId);
}
+
+ mDefaultPermissionsGranted.delete(userId);
+ mFingerprints.remove(userId);
}
private void revokeRuntimePermissionsAndClearFlags(SettingBase sb, int userId) {
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index 76d47a8..e667838 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -36,6 +36,8 @@
/**
* Launcher information used by {@link ShortcutService}.
+ *
+ * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
*/
class ShortcutLauncher extends ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG;
@@ -80,26 +82,31 @@
* Called when the new package can't receive the backup, due to signature or version mismatch.
*/
@Override
- protected void onRestoreBlocked(ShortcutService s) {
+ protected void onRestoreBlocked() {
final ArrayList<PackageWithUser> pinnedPackages =
new ArrayList<>(mPinnedShortcuts.keySet());
mPinnedShortcuts.clear();
for (int i = pinnedPackages.size() - 1; i >= 0; i--) {
final PackageWithUser pu = pinnedPackages.get(i);
- s.getPackageShortcutsLocked(pu.packageName, pu.userId)
- .refreshPinnedFlags(s);
+ final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(pu.packageName);
+ if (p != null) {
+ p.refreshPinnedFlags();
+ }
}
}
@Override
- protected void onRestored(ShortcutService s) {
+ protected void onRestored() {
// Nothing to do.
}
- public void pinShortcuts(@NonNull ShortcutService s, @UserIdInt int packageUserId,
+ public void pinShortcuts(@UserIdInt int packageUserId,
@NonNull String packageName, @NonNull List<String> ids) {
final ShortcutPackage packageShortcuts =
- s.getPackageShortcutsLocked(packageName, packageUserId);
+ mShortcutUser.getPackageShortcutsIfExists(packageName);
+ if (packageShortcuts == null) {
+ return; // No need to instantiate.
+ }
final PackageWithUser pu = PackageWithUser.of(packageUserId, packageName);
@@ -120,13 +127,14 @@
if (si == null) {
continue;
}
- if (si.isDynamic() || (prevSet != null && prevSet.contains(id))) {
+ if (si.isDynamic() || si.isManifestShortcut()
+ || (prevSet != null && prevSet.contains(id))) {
newSet.add(id);
}
}
mPinnedShortcuts.put(pu, newSet);
}
- packageShortcuts.refreshPinnedFlags(s);
+ packageShortcuts.refreshPinnedFlags();
}
/**
@@ -240,7 +248,7 @@
return ret;
}
- public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+ public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println();
pw.print(prefix);
@@ -252,7 +260,7 @@
pw.print(getOwnerUserId());
pw.println();
- getPackageInfo().dump(s, pw, prefix + " ");
+ getPackageInfo().dump(pw, prefix + " ");
pw.println();
final int size = mPinnedShortcuts.size();
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 151f61e..d637586 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -20,15 +20,20 @@
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
+import android.content.res.Resources;
import android.os.PersistableBundle;
import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
+import com.android.server.pm.ShortcutService.ShortcutOperation;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -38,15 +43,23 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
/**
* Package information used by {@link ShortcutService}.
+ * User information used by {@link ShortcutService}.
+ *
+ * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
+ *
+ * TODO Max dynamic shortcuts cap should be per activity.
*/
class ShortcutPackage extends ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG;
+ private static final String TAG_VERIFY = ShortcutService.TAG + ".verify";
static final String TAG_ROOT = "package";
private static final String TAG_INTENT_EXTRAS = "intent-extras";
@@ -55,18 +68,25 @@
private static final String TAG_CATEGORIES = "categories";
private static final String ATTR_NAME = "name";
- private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
private static final String ATTR_CALL_COUNT = "call-count";
private static final String ATTR_LAST_RESET = "last-reset";
private static final String ATTR_ID = "id";
private static final String ATTR_ACTIVITY = "activity";
private static final String ATTR_TITLE = "title";
+ private static final String ATTR_TITLE_RES_ID = "titleid";
+ private static final String ATTR_TITLE_RES_NAME = "titlename";
private static final String ATTR_TEXT = "text";
+ private static final String ATTR_TEXT_RES_ID = "textid";
+ private static final String ATTR_TEXT_RES_NAME = "textname";
+ private static final String ATTR_DISABLED_MESSAGE = "dmessage";
+ private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
+ private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
private static final String ATTR_INTENT = "intent";
- private static final String ATTR_WEIGHT = "weight";
+ private static final String ATTR_RANK = "rank";
private static final String ATTR_TIMESTAMP = "timestamp";
private static final String ATTR_FLAGS = "flags";
- private static final String ATTR_ICON_RES = "icon-res";
+ private static final String ATTR_ICON_RES_ID = "icon-res";
+ private static final String ATTR_ICON_RES_NAME = "icon-resname";
private static final String ATTR_BITMAP_PATH = "bitmap-path";
private static final String NAME_CATEGORIES = "categories";
@@ -80,11 +100,6 @@
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 mApiCallCount;
@@ -98,17 +113,16 @@
private long mLastKnownForegroundElapsedTime;
- private ShortcutPackage(ShortcutService s, ShortcutUser shortcutUser,
+ private ShortcutPackage(ShortcutUser shortcutUser,
int packageUserId, String packageName, ShortcutPackageInfo spi) {
super(shortcutUser, packageUserId, packageName,
spi != null ? spi : ShortcutPackageInfo.newEmpty());
- mPackageUid = s.injectGetPackageUid(packageName, packageUserId);
+ mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
}
- public ShortcutPackage(ShortcutService s, ShortcutUser shortcutUser,
- int packageUserId, String packageName) {
- this(s, shortcutUser, packageUserId, packageName, null);
+ public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
+ this(shortcutUser, packageUserId, packageName, null);
}
@Override
@@ -122,37 +136,46 @@
}
/**
- * Called when a shortcut is about to be published. At this point we know the publisher package
+ * Called when a shortcut is about to be published. At this point we know the publisher
+ * package
* exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so
* we do some initialization for the package.
*/
- private void onShortcutPublish(ShortcutService s) {
+ private void ensurePackageVersionInfo() {
// Make sure we have the version code for the app. We need the version code in
// handlePackageUpdated().
if (getPackageInfo().getVersionCode() < 0) {
- final int versionCode = s.getApplicationVersionCode(getPackageName(), getOwnerUserId());
- if (ShortcutService.DEBUG) {
- Slog.d(TAG, String.format("Package %s version = %d", getPackageName(),
- versionCode));
- }
- if (versionCode >= 0) {
- getPackageInfo().setVersionCode(versionCode);
+ final ShortcutService s = mShortcutUser.mService;
+
+ final PackageInfo pi = s.getPackageInfo(getPackageName(), getOwnerUserId());
+ if (pi != null) {
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format("Package %s version = %d", getPackageName(),
+ pi.versionCode));
+ }
+ getPackageInfo().updateVersionInfo(pi);
s.scheduleSaveUser(getOwnerUserId());
}
}
}
+ @Nullable
+ public Resources getPackageResources() {
+ return mShortcutUser.mService.injectGetResourcesForApplicationAsUser(
+ getPackageName(), getPackageUserId());
+ }
+
@Override
- protected void onRestoreBlocked(ShortcutService s) {
+ protected void onRestoreBlocked() {
// Can't restore due to version/signature mismatch. Remove all shortcuts.
mShortcuts.clear();
}
@Override
- protected void onRestored(ShortcutService s) {
+ protected void onRestored() {
// Because some launchers may not have been restored (e.g. allowBackup=false),
// we need to re-calculate the pinned shortcuts.
- refreshPinnedFlags(s);
+ refreshPinnedFlags();
}
/**
@@ -163,19 +186,48 @@
return mShortcuts.get(id);
}
- private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
- @NonNull String id) {
+ private void ensureNotImmutable(@Nullable ShortcutInfo shortcut) {
+ if (shortcut != null && shortcut.isImmutable()) {
+ throw new IllegalArgumentException(
+ "Manifest shortcut ID=" + shortcut.getId()
+ + " may not be manipulated via APIs");
+ }
+ }
+
+ private void ensureNotImmutable(@NonNull String id) {
+ ensureNotImmutable(mShortcuts.get(id));
+ }
+
+ public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds) {
+ for (int i = shortcutIds.size() - 1; i >= 0; i--) {
+ ensureNotImmutable(shortcutIds.get(i));
+ }
+ }
+
+ public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts) {
+ for (int i = shortcuts.size() - 1; i >= 0; i--) {
+ ensureNotImmutable(shortcuts.get(i).getId());
+ }
+ }
+
+ private ShortcutInfo deleteShortcutInner(@NonNull String id) {
final ShortcutInfo shortcut = mShortcuts.remove(id);
if (shortcut != null) {
- s.removeIcon(getPackageUserId(), shortcut);
- shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
+ mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut);
+ shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
+ | ShortcutInfo.FLAG_MANIFEST);
}
return shortcut;
}
- void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
- deleteShortcut(s, newShortcut.getId());
+ private void addShortcutInner(@NonNull ShortcutInfo newShortcut) {
+ final ShortcutService s = mShortcutUser.mService;
+
+ deleteShortcutInner(newShortcut.getId());
+
+ // Extract Icon and update the icon res ID and the bitmap path.
s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
+ s.fixUpShortcutResourceNamesAndValues(newShortcut);
mShortcuts.put(newShortcut.getId(), newShortcut);
}
@@ -184,52 +236,53 @@
*
* It checks the max number of dynamic shortcuts.
*/
- public void addDynamicShortcut(@NonNull ShortcutService s,
- @NonNull ShortcutInfo newShortcut) {
+ public void addOrUpdateDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
- onShortcutPublish(s);
+ Preconditions.checkArgument(newShortcut.isEnabled(),
+ "add/setDynamicShortcuts() cannot publish disabled shortcuts");
+
+ ensurePackageVersionInfo();
newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
final boolean wasPinned;
- final int newDynamicCount;
if (oldShortcut == null) {
wasPinned = false;
- newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
} else {
+ // It's an update case.
+ // Make sure the target is updatable. (i.e. should be mutable.)
+ oldShortcut.ensureUpdatableWith(newShortcut);
+
wasPinned = oldShortcut.isPinned();
- if (oldShortcut.isDynamic()) {
- newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
- } else {
- newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
+ if (!oldShortcut.isEnabled()) {
+ newShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
}
}
- // Make sure there's still room.
- s.enforceMaxDynamicShortcuts(newDynamicCount);
+ // TODO Check max dynamic count.
+ // mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount);
- // Okay, make it dynamic and add.
+ // If it was originally pinned, the new one should be pinned too.
if (wasPinned) {
newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
}
- addShortcut(s, newShortcut);
- mDynamicShortcutCount = newDynamicCount;
+ addShortcutInner(newShortcut);
}
/**
* Remove all shortcuts that aren't pinned nor dynamic.
*/
- private void removeOrphans(@NonNull ShortcutService s) {
+ private void removeOrphans() {
ArrayList<String> removeList = null; // Lazily initialize.
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = mShortcuts.valueAt(i);
- if (si.isPinned() || si.isDynamic()) continue;
+ if (si.isAlive()) continue;
if (removeList == null) {
removeList = new ArrayList<>();
@@ -238,7 +291,7 @@
}
if (removeList != null) {
for (int i = removeList.size() - 1; i >= 0; i--) {
- deleteShortcut(s, removeList.get(i));
+ deleteShortcutInner(removeList.get(i));
}
}
}
@@ -246,30 +299,85 @@
/**
* Remove all dynamic shortcuts.
*/
- public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
+ public void deleteAllDynamicShortcuts() {
+ final long now = mShortcutUser.mService.injectCurrentTimeMillis();
+
+ boolean changed = false;
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
- mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+ if (si.isDynamic()) {
+ changed = true;
+
+ si.setTimestamp(now);
+ si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+ si.setRank(0); // It may still be pinned, so clear the rank.
+ }
}
- removeOrphans(s);
- mDynamicShortcutCount = 0;
+ if (changed) {
+ removeOrphans();
+ }
}
/**
- * Remove a dynamic shortcut by ID.
+ * Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
+ * is pinned, it'll remain as a pinned shortcut, and is still enabled.
*/
- public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
+ public void deleteDynamicWithId(@NonNull String shortcutId) {
+ deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
+ }
+
+ /**
+ * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
+ * is pinned, it'll remain as a pinned shortcut but will be disabled.
+ */
+ public void disableWithId(@NonNull String shortcutId, String disabledMessage,
+ int disabledMessageResId, boolean overrideImmutable) {
+ final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
+ overrideImmutable);
+
+ if (disabled != null) {
+ if (disabledMessage != null) {
+ disabled.setDisabledMessage(disabledMessage);
+ } else if (disabledMessageResId != 0) {
+ disabled.setDisabledMessageResId(disabledMessageResId);
+
+ mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled);
+ }
+ }
+ }
+
+ @Nullable
+ private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable,
+ boolean overrideImmutable) {
final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
- if (oldShortcut == null) {
- return;
+ if (oldShortcut == null || !oldShortcut.isEnabled()) {
+ return null; // Doesn't exist or already disabled.
}
- if (oldShortcut.isDynamic()) {
- mDynamicShortcutCount--;
+ if (!overrideImmutable) {
+ ensureNotImmutable(oldShortcut);
}
if (oldShortcut.isPinned()) {
- oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+
+ oldShortcut.setRank(0);
+ oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST);
+ if (disable) {
+ oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
+ }
+ oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis());
+
+ return oldShortcut;
} else {
- deleteShortcut(s, shortcutId);
+ deleteShortcutInner(shortcutId);
+ return null;
+ }
+ }
+
+ public void enableWithId(@NonNull String shortcutId) {
+ final ShortcutInfo shortcut = mShortcuts.get(shortcutId);
+ if (shortcut != null) {
+ ensureNotImmutable(shortcut);
+ shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED);
}
}
@@ -279,14 +387,15 @@
*
* <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
*/
- public void refreshPinnedFlags(@NonNull ShortcutService s) {
+ public void refreshPinnedFlags() {
// First, un-pin all shortcuts
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
}
// Then, for the pinned set for each launcher, set the pin flag one by one.
- s.getUserShortcutsLocked(getPackageUserId()).forAllLaunchers(launcherShortcuts -> {
+ mShortcutUser.mService.getUserShortcutsLocked(getPackageUserId())
+ .forAllLaunchers(launcherShortcuts -> {
final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
getPackageName(), getPackageUserId());
@@ -308,7 +417,7 @@
});
// Lastly, remove the ones that are no longer pinned nor dynamic.
- removeOrphans(s);
+ removeOrphans();
}
/**
@@ -317,8 +426,10 @@
* <p>This takes care of the resetting the counter for foreground apps as well as after
* locale changes.
*/
- public int getApiCallCount(@NonNull ShortcutService s) {
- mShortcutUser.resetThrottlingIfNeeded(s);
+ public int getApiCallCount() {
+ mShortcutUser.resetThrottlingIfNeeded();
+
+ final ShortcutService s = mShortcutUser.mService;
// Reset the counter if:
// - the package is in foreground now.
@@ -328,7 +439,7 @@
|| mLastKnownForegroundElapsedTime
< s.getUidLastForegroundElapsedTimeLocked(mPackageUid)) {
mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
- resetRateLimiting(s);
+ resetRateLimiting();
}
// Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
@@ -349,8 +460,8 @@
// If not reset yet, then reset.
if (mLastResetTime < last) {
if (ShortcutService.DEBUG) {
- Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
- mLastResetTime, now, last));
+ Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
+ getPackageName(), mLastResetTime, now, last));
}
mApiCallCount = 0;
mLastResetTime = last;
@@ -365,8 +476,10 @@
* <p>This takes care of the resetting the counter for foreground apps as well as after
* locale changes, which is done internally by {@link #getApiCallCount}.
*/
- public boolean tryApiCall(@NonNull ShortcutService s) {
- if (getApiCallCount(s) >= s.mMaxUpdatesPerInterval) {
+ public boolean tryApiCall() {
+ final ShortcutService s = mShortcutUser.mService;
+
+ if (getApiCallCount() >= s.mMaxUpdatesPerInterval) {
return false;
}
mApiCallCount++;
@@ -374,13 +487,13 @@
return true;
}
- public void resetRateLimiting(@NonNull ShortcutService s) {
+ public void resetRateLimiting() {
if (ShortcutService.DEBUG) {
Slog.d(TAG, "resetRateLimiting: " + getPackageName());
}
if (mApiCallCount > 0) {
mApiCallCount = 0;
- s.scheduleSaveUser(getOwnerUserId());
+ mShortcutUser.mService.scheduleSaveUser(getOwnerUserId());
}
}
@@ -392,9 +505,9 @@
/**
* Find all shortcuts that match {@code query}.
*/
- public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
+ public void findAll(@NonNull List<ShortcutInfo> result,
@Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
- findAll(s, result, query, cloneFlag, null, 0);
+ findAll(result, query, cloneFlag, null, 0);
}
/**
@@ -404,7 +517,7 @@
* by the calling launcher will not be included in the result, and also "isPinned" will be
* adjusted for the caller too.
*/
- public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
+ public void findAll(@NonNull List<ShortcutInfo> result,
@Nullable Predicate<ShortcutInfo> query, int cloneFlag,
@Nullable String callingLauncher, int launcherUserId) {
if (getPackageInfo().isShadow()) {
@@ -412,6 +525,8 @@
return;
}
+ final ShortcutService s = mShortcutUser.mService;
+
// Set of pinned shortcuts by the calling launcher.
final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
: s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
@@ -420,22 +535,19 @@
for (int i = 0; i < mShortcuts.size(); i++) {
final ShortcutInfo si = mShortcuts.valueAt(i);
- // If it's called by non-launcher (i.e. publisher, always include -> true.
- // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
- // it.
+ // Need to adjust PINNED flag depending on the caller.
+ // Basically if the caller is a launcher (callingLauncher != null) and the launcher
+ // isn't pinning it, then we need to clear PINNED for this caller.
final boolean isPinnedByCaller = (callingLauncher == null)
|| ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
- if (!si.isDynamic()) {
- if (!si.isPinned()) {
- s.wtf("Shortcut not pinned: package " + getPackageName()
- + ", user=" + getPackageUserId() + ", id=" + si.getId());
- continue;
- }
+
+ if (si.isFloating()) {
if (!isPinnedByCaller) {
continue;
}
}
final ShortcutInfo clone = si.clone(cloneFlag);
+
// Fix up isPinned for the caller. Note we need to do it before the "test" callback,
// since it may check isPinned.
if (!isPinnedByCaller) {
@@ -452,30 +564,126 @@
}
/**
- * Called when the package is updated. If there are shortcuts with resource icons, update
- * their timestamps.
+ * Return the filenames (excluding path names) of icon bitmap files from this package.
*/
- public void handlePackageUpdated(ShortcutService s, int newVersionCode) {
- if (getPackageInfo().getVersionCode() >= newVersionCode) {
- // Version hasn't changed; nothing to do.
- return;
- }
- if (ShortcutService.DEBUG) {
- Slog.d(TAG, String.format("Package %s updated, version %d -> %d", getPackageName(),
- getPackageInfo().getVersionCode(), newVersionCode));
- }
+ public ArraySet<String> getUsedBitmapFiles() {
+ final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size());
- getPackageInfo().setVersionCode(newVersionCode);
-
- boolean changed = false;
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = mShortcuts.valueAt(i);
-
- if (si.hasIconResource()) {
- changed = true;
- si.setTimestamp(s.injectCurrentTimeMillis());
+ if (si.getBitmapPath() != null) {
+ usedFiles.add(getFileName(si.getBitmapPath()));
}
}
+ return usedFiles;
+ }
+
+ private static String getFileName(@NonNull String path) {
+ final int sep = path.lastIndexOf(File.separatorChar);
+ if (sep == -1) {
+ return path;
+ } else {
+ return path.substring(sep + 1);
+ }
+ }
+
+ /**
+ * Called when the package is updated or added.
+ *
+ * Add case:
+ * - Publish manifest shortcuts.
+ *
+ * Update case:
+ * - Re-publish manifest shortcuts.
+ * - If there are shortcuts with resources (icons or strings), update their timestamps.
+ *
+ * @return TRUE if any shortcuts have been changed.
+ */
+ public boolean handlePackageAddedOrUpdated(boolean isNewApp) {
+ final PackageInfo pi = mShortcutUser.mService.getPackageInfo(
+ getPackageName(), getPackageUserId());
+ if (pi == null) {
+ return false; // Shouldn't happen.
+ }
+
+ if (!isNewApp) {
+ // Make sure the version code or last update time has changed.
+ // Otherwise, nothing to do.
+ if (getPackageInfo().getVersionCode() >= pi.versionCode
+ && getPackageInfo().getLastUpdateTime() >= pi.lastUpdateTime) {
+ return false;
+ }
+ }
+
+ // Now prepare to publish manifest shortcuts.
+ List<ShortcutInfo> newManifestShortcutList = null;
+ try {
+ newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
+ getPackageName(), getPackageUserId());
+ } catch (IOException|XmlPullParserException e) {
+ Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
+ }
+ final int manifestShortcutSize = newManifestShortcutList == null ? 0
+ : newManifestShortcutList.size();
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)",
+ getPackageName(), manifestShortcutSize));
+ }
+ if (isNewApp && (manifestShortcutSize == 0)) {
+ // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
+
+ // If it's an update, then it may already have manifest shortcuts, which need to be
+ // disabled.
+ return false;
+ }
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
+ (isNewApp ? "added" : "updated"),
+ getPackageInfo().getVersionCode(), pi.versionCode));
+ }
+
+ getPackageInfo().updateVersionInfo(pi);
+
+ final ShortcutService s = mShortcutUser.mService;
+
+ boolean changed = false;
+
+ // For existing shortcuts, update timestamps if they have any resources.
+ if (!isNewApp) {
+ Resources publisherRes = null;
+
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+
+ if (si.hasAnyResources()) {
+ if (!si.isOriginallyFromManifest()) {
+ if (publisherRes == null) {
+ publisherRes = getPackageResources();
+ if (publisherRes == null) {
+ break; // Resources couldn't be loaded.
+ }
+ }
+
+ // If this shortcut is not from a manifest, then update all resource IDs
+ // from resource names. (We don't allow resource strings for
+ // non-manifest at the moment, but icons can still be resources.)
+ si.lookupAndFillInResourceIds(publisherRes);
+ }
+ changed = true;
+ si.setTimestamp(s.injectCurrentTimeMillis());
+ }
+ }
+ }
+
+ // (Re-)publish manifest shortcut.
+ changed |= publishManifestShortcuts(newManifestShortcutList);
+
+ if (newManifestShortcutList != null) {
+ changed |= pushOutExcessShortcuts();
+ }
+
+ s.verifyStates();
+
if (changed) {
// This will send a notification to the launcher, and also save .
s.packageShortcutsChanged(getPackageName(), getPackageUserId());
@@ -483,9 +691,388 @@
// Still save the version code.
s.scheduleSaveUser(getPackageUserId());
}
+ return changed;
}
- public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+ private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) {
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format(
+ "Package %s: publishing manifest shortcuts", getPackageName()));
+ }
+ boolean changed = false;
+
+ // Keep the previous IDs.
+ ArraySet<String> toDisableList = null;
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+
+ if (si.isManifestShortcut()) {
+ if (toDisableList == null) {
+ toDisableList = new ArraySet<>();
+ }
+ toDisableList.add(si.getId());
+ }
+ }
+
+ // Publish new ones.
+ if (newManifestShortcutList != null) {
+ final int newListSize = newManifestShortcutList.size();
+
+ for (int i = 0; i < newListSize; i++) {
+ changed = true;
+
+ final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
+ final boolean newDisabled = !newShortcut.isEnabled();
+
+ final String id = newShortcut.getId();
+ final ShortcutInfo oldShortcut = mShortcuts.get(id);
+
+ boolean wasPinned = false;
+
+ if (oldShortcut != null) {
+ if (!oldShortcut.isOriginallyFromManifest()) {
+ Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId()
+ + " exists but is not from AndroidManifest.xml, not updating.");
+ continue;
+ }
+ // Take over the pinned flag.
+ if (oldShortcut.isPinned()) {
+ wasPinned = true;
+ newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
+ }
+ }
+ if (newDisabled && !wasPinned) {
+ // If the shortcut is disabled, and it was *not* pinned, then this
+ // just doesn't have to be published.
+ // Just keep it in toDisableList, so the previous one would be removed.
+ continue;
+ }
+
+ // Note even if enabled=false, we still need to update all fields, so do it
+ // regardless.
+ addShortcutInner(newShortcut); // This will clean up the old one too.
+
+ if (!newDisabled && toDisableList != null) {
+ // Still alive, don't remove.
+ toDisableList.remove(id);
+ }
+ }
+ }
+
+ // Disable the previous manifest shortcuts that are no longer in the manifest.
+ if (toDisableList != null) {
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format(
+ "Package %s: disabling %d stale shortcuts", getPackageName(),
+ toDisableList.size()));
+ }
+ for (int i = toDisableList.size() - 1; i >= 0; i--) {
+ changed = true;
+
+ final String id = toDisableList.valueAt(i);
+
+ disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0,
+ /* overrideImmutable=*/ true);
+ }
+ removeOrphans();
+ }
+ adjustRanks();
+ return changed;
+ }
+
+ /**
+ * For each target activity, make sure # of dynamic + manifest shortcuts <= max.
+ * If too many, we'll remove the dynamic with the lowest ranks.
+ */
+ private boolean pushOutExcessShortcuts() {
+ final ShortcutService service = mShortcutUser.mService;
+ final int maxShortcuts = service.getMaxActivityShortcuts();
+
+ boolean changed = false;
+
+ final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
+ sortShortcutsToActivities();
+ for (int outer = all.size() - 1; outer >= 0; outer--) {
+ final ArrayList<ShortcutInfo> list = all.valueAt(outer);
+ if (list.size() <= maxShortcuts) {
+ continue;
+ }
+ // Sort by isManifestShortcut() and getRank().
+ Collections.sort(list, mShortcutTypeAndRankComparator);
+
+ // Keep [0 .. max), and remove (as dynamic) [max .. size)
+ for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) {
+ final ShortcutInfo shortcut = list.get(inner);
+
+ if (shortcut.isManifestShortcut()) {
+ // This shouldn't happen -- excess shortcuts should all be non-manifest.
+ // But just in case.
+ service.wtf("Found manifest shortcuts in excess list.");
+ continue;
+ }
+ deleteDynamicWithId(shortcut.getId());
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * To sort by isManifestShortcut() and getRank(). i.e. manifest shortcuts come before
+ * non-manifest shortcuts, then sort by rank.
+ *
+ * This is used to decide which dynamic shortcuts to remove when an upgraded version has more
+ * manifest shortcuts than before and as a result we need to remove some of the dynamic
+ * shortcuts. We sort manifest + dynamic shortcuts by this order, and remove the ones with
+ * the last ones.
+ *
+ * (Note the number of manifest shortcuts is always <= the max number, because if there are
+ * more, ShortcutParser would ignore the rest.)
+ */
+ final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a,
+ ShortcutInfo b) -> {
+ if (a.isManifestShortcut() && !b.isManifestShortcut()) {
+ return -1;
+ }
+ if (!a.isManifestShortcut() && b.isManifestShortcut()) {
+ return 1;
+ }
+ return Integer.compare(a.getRank(), b.getRank());
+ };
+
+ /**
+ * Build a list of shortcuts for each target activity and return as a map. The result won't
+ * contain "floating" shortcuts because they don't belong on any activities.
+ */
+ private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() {
+ final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts
+ = new ArrayMap<>();
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+ if (si.isFloating()) {
+ continue; // Ignore floating shortcuts, which are not tied to any activities.
+ }
+
+ final ComponentName activity = si.getActivity();
+
+ ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity);
+ if (list == null) {
+ list = new ArrayList<>();
+ activitiesToShortcuts.put(activity, list);
+ }
+ list.add(si);
+ }
+ return activitiesToShortcuts;
+ }
+
+ /** Used by {@link #enforceShortcutCountsBeforeOperation} */
+ private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts,
+ ComponentName cn, int increment) {
+ Integer oldValue = counts.get(cn);
+ if (oldValue == null) {
+ oldValue = 0;
+ }
+
+ counts.put(cn, oldValue + increment);
+ }
+
+ /**
+ * Called by
+ * {@link android.content.pm.ShortcutManager#setDynamicShortcuts},
+ * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and
+ * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing
+ * the operation to make sure the operation wouldn't result in the target activities having
+ * more than the allowed number of dynamic/manifest shortcuts.
+ *
+ * @param newList shortcut list passed to set, add or updateShortcuts().
+ * @param operation add, set or update.
+ * @throws IllegalArgumentException if the operation would result in going over the max
+ * shortcut count for any activity.
+ */
+ public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList,
+ @ShortcutOperation int operation) {
+ final ShortcutService service = mShortcutUser.mService;
+
+ // Current # of dynamic / manifest shortcuts for each activity.
+ // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced
+ // anyway.)
+ final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4);
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo shortcut = mShortcuts.valueAt(i);
+
+ if (shortcut.isManifestShortcut()) {
+ incrementCountForActivity(counts, shortcut.getActivity(), 1);
+ } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) {
+ incrementCountForActivity(counts, shortcut.getActivity(), 1);
+ }
+ }
+
+ for (int i = newList.size() - 1; i >= 0; i--) {
+ final ShortcutInfo newShortcut = newList.get(i);
+ final ComponentName newActivity = newShortcut.getActivity();
+ if (newActivity == null) {
+ if (operation != ShortcutService.OPERATION_UPDATE) {
+ // This method may be called before validating shortcuts, so this may happen,
+ // and is a caller side error.
+ throw new NullPointerException("Activity must be provided");
+ }
+ continue; // Activity can be null for update.
+ }
+
+ final ShortcutInfo original = mShortcuts.get(newShortcut.getId());
+ if (original == null) {
+ if (operation == ShortcutService.OPERATION_UPDATE) {
+ continue; // When updating, ignore if there's no target.
+ }
+ // Add() or set(), and there's no existing shortcut with the same ID. We're
+ // simply publishing (as opposed to updating) this shortcut, so just +1.
+ incrementCountForActivity(counts, newActivity, 1);
+ continue;
+ }
+ if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) {
+ // Updating floating shortcuts doesn't affect the count, so ignore.
+ continue;
+ }
+
+ // If it's add() or update(), then need to decrement for the previous activity.
+ // Skip it for set() since it's already been taken care of by not counting the original
+ // dynamic shortcuts in the first loop.
+ if (operation != ShortcutService.OPERATION_SET) {
+ final ComponentName oldActivity = original.getActivity();
+ if (!original.isFloating()) {
+ incrementCountForActivity(counts, oldActivity, -1);
+ }
+ }
+ incrementCountForActivity(counts, newActivity, 1);
+ }
+
+ // Then make sure none of the activities have more than the max number of shortcuts.
+ for (int i = counts.size() - 1; i >= 0; i--) {
+ service.enforceMaxActivityShortcuts(counts.valueAt(i));
+ }
+ }
+
+ /**
+ * For all the text fields, refresh the string values if they're from resources.
+ */
+ public void resolveResourceStrings() {
+ final ShortcutService s = mShortcutUser.mService;
+ boolean changed = false;
+
+ Resources publisherRes = null;
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+
+ if (si.hasStringResources()) {
+ changed = true;
+
+ if (publisherRes == null) {
+ publisherRes = getPackageResources();
+ if (publisherRes == null) {
+ break; // Resources couldn't be loaded.
+ }
+ }
+
+ si.resolveResourceStrings(publisherRes);
+ si.setTimestamp(s.injectCurrentTimeMillis());
+ }
+ }
+ if (changed) {
+ s.scheduleSaveUser(getPackageUserId());
+ }
+ }
+
+ /** Clears the implicit ranks for all shortcuts. */
+ public void clearAllImplicitRanks() {
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+ si.clearImplicitRankAndRankChangedFlag();
+ }
+ }
+
+ /**
+ * Used to sort shortcuts for rank auto-adjusting.
+ */
+ final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> {
+ // First, sort by rank.
+ int ret = Integer.compare(a.getRank(), b.getRank());
+ if (ret != 0) {
+ return ret;
+ }
+ // When ranks are tie, then prioritize the ones that have just been assigned new ranks.
+ // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively,
+ // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because
+ // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set.
+ // Similarly, updating s3's rank to 1 will insert it between s1 and s2.
+ if (a.isRankChanged() != b.isRankChanged()) {
+ return a.isRankChanged() ? -1 : 1;
+ }
+ // If they're still tie, sort by implicit rank -- i.e. preserve the order in which
+ // they're passed to the API.
+ ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank());
+ if (ret != 0) {
+ return ret;
+ }
+ // If they're stil tie, just sort by their IDs.
+ // This may happen with updateShortcuts() -- see
+ // the testUpdateShortcuts_noManifestShortcuts() test.
+ return a.getId().compareTo(b.getId());
+ };
+
+ /**
+ * Re-calculate the ranks for all shortcuts.
+ */
+ public void adjustRanks() {
+ final ShortcutService s = mShortcutUser.mService;
+ final long now = s.injectCurrentTimeMillis();
+
+ // First, clear ranks for floating shortcuts.
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+ if (si.isFloating()) {
+ if (si.getRank() != 0) {
+ si.setTimestamp(now);
+ si.setRank(0);
+ }
+ }
+ }
+
+ // Then adjust ranks. Ranks are unique for each activity, so we first need to sort
+ // shortcuts to each activity.
+ // Then sort the shortcuts within each activity with mShortcutRankComparator, and
+ // assign ranks from 0.
+ final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
+ sortShortcutsToActivities();
+ for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity.
+ final ArrayList<ShortcutInfo> list = all.valueAt(outer);
+
+ // Sort by ranks and other signals.
+ Collections.sort(list, mShortcutRankComparator);
+
+ int rank = 0;
+
+ final int size = list.size();
+ for (int i = 0; i < size; i++) {
+ final ShortcutInfo si = list.get(i);
+ if (si.isManifestShortcut()) {
+ // Don't adjust ranks for manifest shortcuts.
+ continue;
+ }
+ // At this point, it must be dynamic.
+ if (!si.isDynamic()) {
+ s.wtf("Non-dynamic shortcut found.");
+ continue;
+ }
+ final int thisRank = rank++;
+ if (si.getRank() != thisRank) {
+ si.setTimestamp(now);
+ si.setRank(thisRank);
+ }
+ }
+ }
+ }
+
+ public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println();
pw.print(prefix);
@@ -498,7 +1085,7 @@
pw.print(prefix);
pw.print(" ");
pw.print("Calls: ");
- pw.print(getApiCallCount(s));
+ pw.print(getApiCallCount());
pw.println();
// getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
@@ -514,10 +1101,10 @@
pw.print("Last reset: [");
pw.print(mLastResetTime);
pw.print("] ");
- pw.print(s.formatTime(mLastResetTime));
+ pw.print(ShortcutService.formatTime(mLastResetTime));
pw.println();
- getPackageInfo().dump(s, pw, prefix + " ");
+ getPackageInfo().dump(pw, prefix + " ");
pw.println();
pw.print(prefix);
@@ -545,7 +1132,7 @@
pw.print("Total bitmap size: ");
pw.print(totalBitmapSize);
pw.print(" (");
- pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
+ pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize));
pw.println(")");
}
@@ -561,7 +1148,6 @@
out.startTag(null, TAG_ROOT);
ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
- ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
getPackageInfo().saveToXml(out);
@@ -583,12 +1169,20 @@
out.startTag(null, TAG_SHORTCUT);
ShortcutService.writeAttr(out, ATTR_ID, si.getId());
// writeAttr(out, "package", si.getPackageName()); // not needed
- ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
+ ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity());
// writeAttr(out, "icon", si.getIcon()); // We don't save it.
ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
+ ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
+ ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName());
ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
+ ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
+ ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName());
+ ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
+ ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
+ si.getDisabledMessageResourceId());
+ ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME,
+ si.getDisabledMessageResName());
ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
- ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
si.getLastChangedTimestamp());
if (forBackup) {
@@ -598,8 +1192,13 @@
~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
| ShortcutInfo.FLAG_DYNAMIC));
} else {
+ // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored
+ // as dynamic.
+ ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
+
ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
- ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
+ ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId());
+ ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName());
ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
}
@@ -627,11 +1226,9 @@
final String packageName = ShortcutService.parseStringAttribute(parser,
ATTR_NAME);
- final ShortcutPackage ret = new ShortcutPackage(s, shortcutUser,
+ final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
shortcutUser.getUserId(), packageName);
- ret.mDynamicShortcutCount =
- ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
ret.mApiCallCount =
ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
ret.mLastResetTime =
@@ -671,14 +1268,22 @@
ComponentName activityComponent;
// Icon icon;
String title;
+ int titleResId;
+ String titleResName;
String text;
+ int textResId;
+ String textResName;
+ String disabledMessage;
+ int disabledMessageResId;
+ String disabledMessageResName;
Intent intent;
PersistableBundle intentPersistableExtras = null;
- int weight;
+ int rank;
PersistableBundle extras = null;
long lastChangedTimestamp;
int flags;
- int iconRes;
+ int iconResId;
+ String iconResName;
String bitmapPath;
ArraySet<String> categories = null;
@@ -686,12 +1291,22 @@
activityComponent = ShortcutService.parseComponentNameAttribute(parser,
ATTR_ACTIVITY);
title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
+ titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
+ titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
+ textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
+ textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
+ disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
+ disabledMessageResId = ShortcutService.parseIntAttribute(parser,
+ ATTR_DISABLED_MESSAGE_RES_ID);
+ disabledMessageResName = ShortcutService.parseStringAttribute(parser,
+ ATTR_DISABLED_MESSAGE_RES_NAME);
intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
- weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
+ rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
- iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
+ iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID);
+ iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME);
bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
final int outerDepth = parser.getDepth();
@@ -733,14 +1348,97 @@
}
return new ShortcutInfo(
- userId, id, packageName, activityComponent, /* icon =*/ null, title, text,
+ userId, id, packageName, activityComponent, /* icon =*/ null,
+ title, titleResId, titleResName, text, textResId, textResName,
+ disabledMessage, disabledMessageResId, disabledMessageResName,
categories, intent,
- intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
- iconRes, bitmapPath);
+ intentPersistableExtras, rank, extras, lastChangedTimestamp, flags,
+ iconResId, iconResName, bitmapPath);
}
@VisibleForTesting
List<ShortcutInfo> getAllShortcutsForTest() {
return new ArrayList<>(mShortcuts.values());
}
+
+ @Override
+ public void verifyStates() {
+ super.verifyStates();
+
+ boolean failed = false;
+
+ final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
+ sortShortcutsToActivities();
+
+ // Make sure each activity won't have more than max shortcuts.
+ for (int outer = all.size() - 1; outer >= 0; outer--) {
+ final ArrayList<ShortcutInfo> list = all.valueAt(outer);
+ if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer)
+ + " has " + all.valueAt(outer).size() + " shortcuts.");
+ }
+
+ // Sort by rank.
+ Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank()));
+
+ // Split into two arrays for each kind.
+ final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list);
+ dynamicList.removeIf((si) -> !si.isDynamic());
+
+ final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list);
+ dynamicList.removeIf((si) -> !si.isManifestShortcut());
+
+ verifyRanksSequential(dynamicList);
+ verifyRanksSequential(manifestList);
+ }
+
+ // Verify each shortcut's status.
+ for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = mShortcuts.valueAt(i);
+ if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " is not manifest, dynamic or pinned.");
+ }
+ if (si.isManifestShortcut() && si.isDynamic()) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " is both dynamic and manifest at the same time.");
+ }
+ if (si.getActivity() == null) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " has null activity.");
+ }
+ if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " is not floating, but is disabled.");
+ }
+ if (si.isFloating() && si.getRank() != 0) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " is floating, but has rank=" + si.getRank());
+ }
+ }
+
+ if (failed) {
+ throw new IllegalStateException("See logcat for errors");
+ }
+ }
+
+ private boolean verifyRanksSequential(List<ShortcutInfo> list) {
+ boolean failed = false;
+
+ for (int i = 0; i < list.size(); i++) {
+ final ShortcutInfo si = list.get(i);
+ if (si.getRank() != i) {
+ failed = true;
+ Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+ + " rank=" + si.getRank() + " but expected to be "+ i);
+ }
+ }
+ return failed;
+ }
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
index 74969f0..7f5d931 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
@@ -15,6 +15,7 @@
*/
package com.android.server.pm;
+import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.pm.PackageInfo;
import android.util.Slog;
@@ -34,12 +35,15 @@
/**
* Package information used by {@link android.content.pm.ShortcutManager} for backup / restore.
+ *
+ * All methods should be guarded by {@code ShortcutService.mLock}.
*/
class ShortcutPackageInfo {
private static final String TAG = ShortcutService.TAG;
static final String TAG_ROOT = "package-info";
private static final String ATTR_VERSION = "version";
+ private static final String ATTR_LAST_UPDATE_TIME = "last_udpate_time";
private static final String ATTR_SHADOW = "shadow";
private static final String TAG_SIGNATURE = "signature";
@@ -53,16 +57,20 @@
*/
private boolean mIsShadow;
private int mVersionCode = VERSION_UNKNOWN;
+ private long mLastUpdateTime;
private ArrayList<byte[]> mSigHashes;
- private ShortcutPackageInfo(int versionCode, ArrayList<byte[]> sigHashes, boolean isShadow) {
+ private ShortcutPackageInfo(int versionCode, long lastUpdateTime,
+ ArrayList<byte[]> sigHashes, boolean isShadow) {
mVersionCode = versionCode;
+ mLastUpdateTime = lastUpdateTime;
mIsShadow = isShadow;
mSigHashes = sigHashes;
}
public static ShortcutPackageInfo newEmpty() {
- return new ShortcutPackageInfo(VERSION_UNKNOWN, new ArrayList<>(0), /* isShadow */ false);
+ return new ShortcutPackageInfo(VERSION_UNKNOWN, /* last update time =*/ 0,
+ new ArrayList<>(0), /* isShadow */ false);
}
public boolean isShadow() {
@@ -77,8 +85,15 @@
return mVersionCode;
}
- public void setVersionCode(int versionCode) {
- mVersionCode = versionCode;
+ public long getLastUpdateTime() {
+ return mLastUpdateTime;
+ }
+
+ public void updateVersionInfo(@NonNull PackageInfo pi) {
+ if (pi != null) {
+ mVersionCode = pi.versionCode;
+ mLastUpdateTime = pi.lastUpdateTime;
+ }
}
public boolean hasSignatures() {
@@ -111,7 +126,7 @@
Slog.e(TAG, "Can't get signatures: package=" + packageName);
return null;
}
- final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.versionCode,
+ final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.versionCode, pi.lastUpdateTime,
BackupUtils.hashSignatureArray(pi.signatures), /* shadow=*/ false);
return ret;
@@ -131,6 +146,7 @@
return;
}
mVersionCode = pi.versionCode;
+ mLastUpdateTime = pi.lastUpdateTime;
mSigHashes = BackupUtils.hashSignatureArray(pi.signatures);
}
@@ -139,6 +155,7 @@
out.startTag(null, TAG_ROOT);
ShortcutService.writeAttr(out, ATTR_VERSION, mVersionCode);
+ ShortcutService.writeAttr(out, ATTR_LAST_UPDATE_TIME, mLastUpdateTime);
ShortcutService.writeAttr(out, ATTR_SHADOW, mIsShadow);
for (int i = 0; i < mSigHashes.size(); i++) {
@@ -154,6 +171,9 @@
final int versionCode = ShortcutService.parseIntAttribute(parser, ATTR_VERSION);
+ final long lastUpdateTime = ShortcutService.parseIntAttribute(
+ parser, ATTR_LAST_UPDATE_TIME);
+
// When restoring from backup, it's always shadow.
final boolean shadow =
fromBackup || ShortcutService.parseBooleanAttribute(parser, ATTR_SHADOW);
@@ -185,11 +205,12 @@
// Successfully loaded; replace the feilds.
mVersionCode = versionCode;
+ mLastUpdateTime = lastUpdateTime;
mIsShadow = shadow;
mSigHashes = hashes;
}
- public void dump(ShortcutService s, PrintWriter pw, String prefix) {
+ public void dump(PrintWriter pw, String prefix) {
pw.println();
pw.print(prefix);
@@ -205,6 +226,11 @@
pw.print(mVersionCode);
pw.println();
+ pw.print(prefix);
+ pw.print(" Last package update time: ");
+ pw.print(mLastUpdateTime);
+ pw.println();
+
for (int i = 0; i < mSigHashes.size(); i++) {
pw.print(prefix);
pw.print(" ");
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 6fbdb82..757dd19 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -26,6 +26,9 @@
import java.io.IOException;
+/**
+ * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
+ */
abstract class ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG;
@@ -69,18 +72,20 @@
return mPackageInfo;
}
- public void refreshPackageInfoAndSave(ShortcutService s) {
+ public void refreshPackageInfoAndSave() {
if (mPackageInfo.isShadow()) {
return; // Don't refresh for shadow user.
}
+ final ShortcutService s = mShortcutUser.mService;
mPackageInfo.refresh(s, this);
s.scheduleSaveUser(getOwnerUserId());
}
- public void attemptToRestoreIfNeededAndSave(ShortcutService s) {
+ public void attemptToRestoreIfNeededAndSave() {
if (!mPackageInfo.isShadow()) {
return; // Already installed, nothing to do.
}
+ final ShortcutService s = mShortcutUser.mService;
if (!s.isPackageInstalled(mPackageName, mPackageUserId)) {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format("Package still not installed: %s user=%d",
@@ -91,14 +96,14 @@
if (!mPackageInfo.hasSignatures()) {
s.wtf("Attempted to restore package " + mPackageName + ", user=" + mPackageUserId
+ " but signatures not found in the restore data.");
- onRestoreBlocked(s);
+ onRestoreBlocked();
return;
}
final PackageInfo pi = s.getPackageInfoWithSignatures(mPackageName, mPackageUserId);
if (!mPackageInfo.canRestoreTo(s, pi)) {
// Package is now installed, but can't restore. Let the subclass do the cleanup.
- onRestoreBlocked(s);
+ onRestoreBlocked();
return;
}
if (ShortcutService.DEBUG) {
@@ -106,7 +111,7 @@
mPackageUserId, getOwnerUserId()));
}
- onRestored(s);
+ onRestored();
// Now the package is not shadow.
mPackageInfo.setShadow(false);
@@ -118,13 +123,19 @@
* Called when the new package can't be restored because it has a lower version number
* or different signatures.
*/
- protected abstract void onRestoreBlocked(ShortcutService s);
+ protected abstract void onRestoreBlocked();
/**
* Called when the new package is successfully restored.
*/
- protected abstract void onRestored(ShortcutService s);
+ protected abstract void onRestored();
public abstract void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException;
+
+ /**
+ * Verify various internal states.
+ */
+ public void verifyStates() {
+ }
}
diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java
new file mode 100644
index 0000000..c349b75
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -0,0 +1,324 @@
+/*
+ * 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.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class ShortcutParser {
+ private static final String TAG = ShortcutService.TAG;
+
+ private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE
+
+ @VisibleForTesting
+ static final String METADATA_KEY = "android.app.shortcuts";
+
+ private static final String TAG_SHORTCUTS = "shortcuts";
+ private static final String TAG_SHORTCUT = "shortcut";
+ private static final String TAG_INTENT = "intent";
+ private static final String TAG_CATEGORIES = "categories";
+
+ @Nullable
+ public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
+ String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
+ final PackageInfo pi = service.injectGetActivitiesWithMetadata(packageName, userId);
+
+ List<ShortcutInfo> result = null;
+
+ try {
+ if (pi != null && pi.activities != null) {
+ for (ActivityInfo activityInfo : pi.activities) {
+ result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result);
+ }
+ }
+ } catch (RuntimeException e) {
+ // Resource ID mismatch may cause various runtime exceptions when parsing XMLs.
+ service.wtf(
+ "Exception caught while parsing shortcut XML for package=" + packageName, e);
+ return null;
+ }
+ return result;
+ }
+
+ private static List<ShortcutInfo> parseShortcutsOneFile(
+ ShortcutService service,
+ ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
+ List<ShortcutInfo> result) throws IOException, XmlPullParserException {
+ XmlResourceParser parser = null;
+ try {
+ parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
+ if (parser == null) {
+ return result;
+ }
+
+ final ComponentName activity = new ComponentName(packageName, activityInfo.name);
+
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+
+ int rank = 0;
+ final int maxShortcuts = service.getMaxActivityShortcuts();
+ int numShortcuts = 0;
+
+ // We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>,
+ // after parsing <intent>. We keep the current one in here.
+ ShortcutInfo currentShortcut = null;
+
+ Set<String> categories = null;
+
+ outer:
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
+ final int depth = parser.getDepth();
+ final String tag = parser.getName();
+
+ // When a shortcut tag is closing, publish.
+ if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) {
+ if (currentShortcut == null) {
+ // Shortcut was invalid.
+ continue;
+ }
+ final ShortcutInfo si = currentShortcut;
+ currentShortcut = null; // Make sure to null out for the next iteration.
+
+ if (si.getIntent() == null) {
+ Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it.");
+ continue;
+ }
+
+ if (numShortcuts >= maxShortcuts) {
+ Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for "
+ + activityInfo.getComponentName() + ". Skipping the rest.");
+ return result;
+ }
+ if (categories != null) {
+ si.setCategories(categories);
+ categories = null;
+ }
+
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+ result.add(si);
+ numShortcuts++;
+ rank++;
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, "Shortcut added: " + si.toInsecureString());
+ }
+ continue;
+ }
+
+ // Otherwise, just look at start tags.
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth == 1 && TAG_SHORTCUTS.equals(tag)) {
+ continue; // Root tag.
+ }
+ if (depth == 2 && TAG_SHORTCUT.equals(tag)) {
+ final ShortcutInfo si = parseShortcutAttributes(
+ service, attrs, packageName, activity, userId, rank);
+ if (si == null) {
+ // Shortcut was invalid.
+ continue;
+ }
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, "Shortcut found: " + si.toInsecureString());
+ }
+ if (result != null) {
+ for (int i = result.size() - 1; i >= 0; i--) {
+ if (si.getId().equals(result.get(i).getId())) {
+ Log.e(TAG, "Duplicate shortcut ID detected. Skipping it.");
+ continue outer;
+ }
+ }
+ }
+ if (!si.isEnabled()) {
+ // Just set the default intent to disabled shortcuts.
+ si.setIntent(new Intent(Intent.ACTION_VIEW));
+ }
+ currentShortcut = si;
+ categories = null;
+ continue;
+ }
+ if (depth == 3 && TAG_INTENT.equals(tag)) {
+ if ((currentShortcut == null)
+ || (currentShortcut.getIntentNoExtras() != null)
+ || !currentShortcut.isEnabled()) {
+ Log.e(TAG, "Ignoring excessive intent tag.");
+ continue;
+ }
+
+ final Intent intent = Intent.parseIntent(service.mContext.getResources(),
+ parser, attrs);
+ if (TextUtils.isEmpty(intent.getAction())) {
+ Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity);
+ continue;
+ }
+ try {
+ currentShortcut.setIntent(intent);
+ } catch (RuntimeException e) {
+ // This shouldn't happen because intents in XML can't have complicated
+ // extras, but just in case Intent.parseIntent() supports such a thing one
+ // day.
+ Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it.");
+ continue;
+ }
+ continue;
+ }
+ if (depth == 3 && TAG_CATEGORIES.equals(tag)) {
+ if ((currentShortcut == null)
+ || (currentShortcut.getCategories() != null)) {
+ continue;
+ }
+ final String name = parseCategories(service, attrs);
+ if (TextUtils.isEmpty(name)) {
+ Log.e(TAG, "Empty category found. activity=" + activity);
+ continue;
+ }
+
+ if (categories == null) {
+ categories = new ArraySet<>();
+ }
+ categories.add(name);
+ continue;
+ }
+
+ Log.w(TAG, "Unknown tag " + tag + " at depth " + depth);
+ }
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ return result;
+ }
+
+ private static String parseCategories(ShortcutService service, AttributeSet attrs) {
+ final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
+ R.styleable.ShortcutCategories);
+ try {
+ return sa.getString(R.styleable.ShortcutCategories_name);
+ } finally {
+ sa.recycle();
+ }
+ }
+
+ private static ShortcutInfo parseShortcutAttributes(ShortcutService service,
+ AttributeSet attrs, String packageName, ComponentName activity,
+ @UserIdInt int userId, int rank) {
+ final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
+ R.styleable.Shortcut);
+ try {
+ final String id = sa.getString(R.styleable.Shortcut_shortcutId);
+ final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true);
+ final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0);
+ final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0);
+ final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0);
+ final int disabledMessageResId = sa.getResourceId(
+ R.styleable.Shortcut_shortcutDisabledMessage, 0);
+
+ if (TextUtils.isEmpty(id)) {
+ Slog.w(TAG, "Shortcut ID must be provided. activity=" + activity);
+ return null;
+ }
+ if (titleResId == 0) {
+ Slog.w(TAG, "Shortcut title must be provided. activity=" + activity);
+ return null;
+ }
+
+ return createShortcutFromManifest(
+ service,
+ userId,
+ id,
+ packageName,
+ activity,
+ titleResId,
+ textResId,
+ disabledMessageResId,
+ rank,
+ iconResId,
+ enabled);
+ } finally {
+ sa.recycle();
+ }
+ }
+
+ private static ShortcutInfo createShortcutFromManifest(ShortcutService service,
+ @UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
+ int titleResId, int textResId, int disabledMessageResId,
+ int rank, int iconResId, boolean enabled) {
+
+ final int flags =
+ (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED)
+ | ShortcutInfo.FLAG_IMMUTABLE
+ | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
+
+ // Note we don't need to set resource names here yet. They'll be set when they're about
+ // to be published.
+ return new ShortcutInfo(
+ userId,
+ id,
+ packageName,
+ activityComponent,
+ null, // icon
+ null, // title string
+ titleResId,
+ null, // title res name
+ null, // text string
+ textResId,
+ null, // text res name
+ null, // disabled message string
+ disabledMessageResId,
+ null, // disabled message res name
+ null, // categories
+ null, // intent
+ null, // intent extras
+ rank,
+ null, // extras
+ service.injectCurrentTimeMillis(),
+ flags,
+ iconResId,
+ null, // icon res name
+ null); // bitmap path
+ }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 8268776..be8eeed 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -15,6 +15,7 @@
*/
package com.android.server.pm;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -22,9 +23,11 @@
import android.app.ActivityManagerNative;
import android.app.AppGlobals;
import android.app.IUidObserver;
+import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.IShortcutService;
@@ -32,12 +35,15 @@
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
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.res.Resources;
+import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
@@ -63,6 +69,7 @@
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.KeyValueListParser;
+import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -74,7 +81,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
@@ -100,33 +106,49 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* TODO:
+ * - Deal with the async nature of PACKAGE_ADD. Basically when a publisher does anything after
+ * it's upgraded, the manager should make sure the upgrade process has been executed.
+ *
+ * - HandleUnlockUser needs to be async. Wait on it in onCleanupUser.
+ *
+ * - Implement reportShortcutUsed().
+ *
+ * - validateForXml() should be removed.
+ *
+ * - Ranks should be recalculated after each update.
+ *
+ * - When the system locale changes, update timestamps for shortcuts with string resources,
+ * and notify the launcher. Right now, it resets the throttling, but timestamps are not changed
+ * and there's no notification either.
+ *
+ * - getIconMaxWidth()/getIconMaxHeight() should use xdpi and ydpi.
*
* - Default launcher check does take a few ms. Worth caching.
*
- * - Clear data -> remove all dynamic? but not the pinned?
- *
- * - Scan and remove orphan bitmaps (just in case).
- *
* - Detect when already registered instances are passed to APIs again, which might break
* internal bitmap handling.
*
* - Add more call stats.
+ *
+ * - Rename mMaxDynamicShortcuts, because it includes manifest shortcuts too.
*/
public class ShortcutService extends IShortcutService.Stub {
static final String TAG = "ShortcutService";
- public static final boolean FEATURE_ENABLED = false;
-
static final boolean DEBUG = false; // STOPSHIP if true
static final boolean DEBUG_LOAD = false; // STOPSHIP if true
static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
@@ -200,7 +222,7 @@
String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram";
/**
- * Key name for the max dynamic shortcuts per app. (int)
+ * Key name for the max dynamic shortcuts per activity. (int)
*/
String KEY_MAX_SHORTCUTS = "max_shortcuts";
@@ -261,6 +283,7 @@
private final IPackageManager mIPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final UserManager mUserManager;
+ private final UsageStatsManagerInternal mUsageStatsManagerInternal;
@GuardedBy("mLock")
final SparseIntArray mUidState = new SparseIntArray();
@@ -280,6 +303,8 @@
*/
private final AtomicLong mLocaleChangeSequenceNumber = new AtomicLong();
+ private final AtomicBoolean mBootCompleted = new AtomicBoolean();
+
private static final int PACKAGE_MATCH_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
@@ -293,8 +318,14 @@
int GET_PACKAGE_INFO_WITH_SIG = 2;
int GET_APPLICATION_INFO = 3;
int LAUNCHER_PERMISSION_CHECK = 4;
+ int CLEANUP_DANGLING_BITMAPS = 5;
+ int GET_ACTIVITIES_WITH_METADATA = 6;
+ int GET_INSTALLED_PACKAGES = 7;
+ int CHECK_PACKAGE_CHANGES = 8;
+ int GET_APPLICATION_RESOURCES = 9;
+ int RESOURCE_NAME_LOOKUP = 10;
- int COUNT = LAUNCHER_PERMISSION_CHECK + 1;
+ int COUNT = RESOURCE_NAME_LOOKUP + 1;
}
final Object mStatLock = new Object();
@@ -308,6 +339,19 @@
private static final int PROCESS_STATE_FOREGROUND_THRESHOLD =
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ static final int OPERATION_SET = 0;
+ static final int OPERATION_ADD = 1;
+ static final int OPERATION_UPDATE = 2;
+
+ /** @hide */
+ @IntDef(value = {
+ OPERATION_SET,
+ OPERATION_ADD,
+ OPERATION_UPDATE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ShortcutOperation {}
+
public ShortcutService(Context context) {
this(context, BackgroundThread.get().getLooper());
}
@@ -318,12 +362,12 @@
LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
mHandler = new Handler(looper);
mIPackageManager = AppGlobals.getPackageManager();
- mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
- mUserManager = context.getSystemService(UserManager.class);
+ mPackageManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(PackageManagerInternal.class));
+ mUserManager = Preconditions.checkNotNull(context.getSystemService(UserManager.class));
+ mUsageStatsManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(UsageStatsManagerInternal.class));
- if (!FEATURE_ENABLED) {
- return;
- }
mPackageMonitor.register(context, looper, UserHandle.ALL, /* externalStorage= */ false);
injectRegisterUidObserver(mUidObserver, ActivityManager.UID_OBSERVER_PROCSTATE
@@ -333,7 +377,7 @@
void logDurationStat(int statId, long start) {
synchronized (mStatLock) {
mCountStats[statId]++;
- mDurationStats[statId] += (System.currentTimeMillis() - start);
+ mDurationStats[statId] += (injectElapsedRealtime() - start);
}
}
@@ -424,7 +468,6 @@
/** lifecycle event */
void onBootPhase(int phase) {
- // We want to call initialize() to initialize the configurations, so we don't disable this.
if (DEBUG) {
Slog.d(TAG, "onBootPhase: " + phase);
}
@@ -432,13 +475,16 @@
case SystemService.PHASE_LOCK_SETTINGS_READY:
initialize();
break;
+ case SystemService.PHASE_BOOT_COMPLETED:
+ mBootCompleted.set(true);
+ break;
}
}
/** lifecycle event */
void handleUnlockUser(int userId) {
- if (!FEATURE_ENABLED) {
- return;
+ if (DEBUG) {
+ Slog.d(TAG, "handleUnlockUser: user=" + userId);
}
synchronized (mLock) {
// Preload
@@ -450,9 +496,6 @@
/** lifecycle event */
void handleCleanupUser(int userId) {
- if (!FEATURE_ENABLED) {
- return;
- }
synchronized (mLock) {
unloadUserLocked(userId);
}
@@ -641,10 +684,10 @@
out.endTag(null, tag);
}
- static void writeAttr(XmlSerializer out, String name, String value) throws IOException {
+ static void writeAttr(XmlSerializer out, String name, CharSequence value) throws IOException {
if (TextUtils.isEmpty(value)) return;
- out.attribute(null, name, value);
+ out.attribute(null, name, value.toString());
}
static void writeAttr(XmlSerializer out, String name, long value) throws IOException {
@@ -782,7 +825,7 @@
out.setOutput(bos, StandardCharsets.UTF_8.name());
out.startDocument(null, true);
- getUserShortcutsLocked(userId).saveToXml(this, out, forBackup);
+ getUserShortcutsLocked(userId).saveToXml(out, forBackup);
out.endDocument();
@@ -816,7 +859,9 @@
return null;
}
try {
- return loadUserInternal(userId, in, /* forBackup= */ false);
+ final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
+ cleanupDanglingBitmapDirectoriesLocked(userId, ret);
+ return ret;
} catch (IOException|XmlPullParserException e) {
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
return null;
@@ -958,7 +1003,7 @@
if (userPackages == null) {
userPackages = loadUserLocked(userId);
if (userPackages == null) {
- userPackages = new ShortcutUser(userId);
+ userPackages = new ShortcutUser(this, userId);
}
mUsers.put(userId, userPackages);
}
@@ -976,7 +1021,7 @@
@NonNull
ShortcutPackage getPackageShortcutsLocked(
@NonNull String packageName, @UserIdInt int userId) {
- return getUserShortcutsLocked(userId).getPackageShortcuts(this, packageName);
+ return getUserShortcutsLocked(userId).getPackageShortcuts(packageName);
}
@GuardedBy("mLock")
@@ -985,7 +1030,7 @@
@NonNull String packageName, @UserIdInt int ownerUserId,
@UserIdInt int launcherUserId) {
return getUserShortcutsLocked(ownerUserId)
- .getLauncherShortcuts(this, packageName, launcherUserId);
+ .getLauncherShortcuts(packageName, launcherUserId);
}
// === Caller validation ===
@@ -1013,6 +1058,57 @@
}
}
+ private void cleanupDanglingBitmapDirectoriesLocked(
+ @UserIdInt int userId, @NonNull ShortcutUser user) {
+ if (DEBUG) {
+ Slog.d(TAG, "cleanupDanglingBitmaps: userId=" + userId);
+ }
+ final long start = injectElapsedRealtime();
+
+ final File bitmapDir = getUserBitmapFilePath(userId);
+ final File[] children = bitmapDir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (File child : children) {
+ if (!child.isDirectory()) {
+ continue;
+ }
+ final String packageName = child.getName();
+ if (DEBUG) {
+ Slog.d(TAG, "cleanupDanglingBitmaps: Found directory=" + packageName);
+ }
+ if (!user.hasPackage(packageName)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing dangling bitmap directory: " + packageName);
+ }
+ cleanupBitmapsForPackage(userId, packageName);
+ } else {
+ cleanupDanglingBitmapFilesLocked(userId, user, packageName, child);
+ }
+ }
+ logDurationStat(Stats.CLEANUP_DANGLING_BITMAPS, start);
+ }
+
+ private void cleanupDanglingBitmapFilesLocked(@UserIdInt int userId, @NonNull ShortcutUser user,
+ @NonNull String packageName, @NonNull File path) {
+ final ArraySet<String> usedFiles =
+ user.getPackageShortcuts(packageName).getUsedBitmapFiles();
+
+ for (File child : path.listFiles()) {
+ if (!child.isFile()) {
+ continue;
+ }
+ final String name = child.getName();
+ if (!usedFiles.contains(name)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
+ }
+ child.delete();
+ }
+ }
+ }
+
@VisibleForTesting
static class FileOutputStreamWithPath extends FileOutputStream {
private final File mFile;
@@ -1036,7 +1132,7 @@
FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
throws IOException {
final File packagePath = new File(getUserBitmapFilePath(userId),
- shortcut.getPackageName());
+ shortcut.getPackage());
if (!packagePath.isDirectory()) {
packagePath.mkdirs();
if (!packagePath.isDirectory()) {
@@ -1075,7 +1171,6 @@
}
Bitmap bitmap;
- Bitmap bitmapToRecycle = null;
try {
switch (icon.getType()) {
case Icon.TYPE_RESOURCE: {
@@ -1127,9 +1222,6 @@
}
}
} finally {
- if (bitmapToRecycle != null) {
- bitmapToRecycle.recycle();
- }
// Once saved, we won't use the original icon information, so null it out.
shortcut.clearIcon();
}
@@ -1142,7 +1234,7 @@
// so override in unit tests.
// TODO CTS this case.
void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
- if (!shortcut.getPackageName().equals(icon.getResPackage())) {
+ if (!shortcut.getPackage().equals(icon.getResPackage())) {
throw new IllegalArgumentException(
"Icon resource must reside in shortcut owner package");
}
@@ -1179,6 +1271,24 @@
return scaledBitmap;
}
+ /**
+ * For a shortcut, update all resource names from resource IDs, and also update all
+ * resource-based strings.
+ */
+ void fixUpShortcutResourceNamesAndValues(ShortcutInfo si) {
+ final Resources publisherRes = injectGetResourcesForApplicationAsUser(
+ si.getPackage(), si.getUserId());
+ if (publisherRes != null) {
+ final long start = injectElapsedRealtime();
+ try {
+ si.lookupAndFillInResourceNames(publisherRes);
+ } finally {
+ logDurationStat(Stats.RESOURCE_NAME_LOOKUP, start);
+ }
+ si.resolveResourceStrings(publisherRes);
+ }
+ }
+
// === Caller validation ===
private boolean isCallerSystem() {
@@ -1241,20 +1351,29 @@
throw new SecurityException("Calling package name mismatch");
}
- void postToHandler(Runnable r) {
+ // Overridden in unit tests to execute r synchronously.
+ void injectPostToHandler(Runnable r) {
mHandler.post(r);
}
/**
- * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
+ * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
+ * {@link #getMaxActivityShortcuts()}.
*/
- void enforceMaxDynamicShortcuts(int numShortcuts) {
+ void enforceMaxActivityShortcuts(int numShortcuts) {
if (numShortcuts > mMaxDynamicShortcuts) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
+ * Return the max number of dynamic + manifest shortcuts for each launcher icon.
+ */
+ int getMaxActivityShortcuts() {
+ return mMaxDynamicShortcuts;
+ }
+
+ /**
* - Sends a notification to LauncherApps
* - Write to file
*/
@@ -1268,10 +1387,15 @@
}
private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
- if (!mUserManager.isUserRunning(userId)) {
- return;
+ final long token = injectClearCallingIdentity();
+ try {
+ if (!mUserManager.isUserRunning(userId)) {
+ return;
+ }
+ } finally {
+ injectRestoreCallingIdentity(token);
}
- postToHandler(() -> {
+ injectPostToHandler(() -> {
final ArrayList<ShortcutChangeListener> copy;
synchronized (mLock) {
copy = new ArrayList<>(mListeners);
@@ -1294,10 +1418,9 @@
*/
private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
Preconditions.checkNotNull(shortcut, "Null shortcut detected");
- if (shortcut.getActivityComponent() != null) {
+ if (shortcut.getActivity() != null) {
Preconditions.checkState(
- shortcut.getPackageName().equals(
- shortcut.getActivityComponent().getPackageName()),
+ shortcut.getPackage().equals(shortcut.getActivity().getPackageName()),
"Activity package name mismatch");
}
@@ -1340,7 +1463,7 @@
}
}
- private static void validateForXml(String s) {
+ private static void validateForXml(CharSequence s) {
if (TextUtils.isEmpty(s)) {
return;
}
@@ -1355,6 +1478,12 @@
return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
}
+ private void assignImplicitRanks(List<ShortcutInfo> shortcuts) {
+ for (int i = shortcuts.size() - 1; i >= 0; i--) {
+ shortcuts.get(i).setImplicitRank(i);
+ }
+ }
+
// === APIs ===
@Override
@@ -1368,27 +1497,39 @@
synchronized (mLock) {
final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+ ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+
+ ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
+
// Throttling.
- if (!ps.tryApiCall(this)) {
+ if (!ps.tryApiCall()) {
return false;
}
- enforceMaxDynamicShortcuts(size);
- // Validate the shortcuts.
+ // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
+ ps.clearAllImplicitRanks();
+ assignImplicitRanks(newShortcuts);
+
for (int i = 0; i < size; i++) {
fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
}
// First, remove all un-pinned; dynamic shortcuts
- ps.deleteAllDynamicShortcuts(this);
+ 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);
- ps.addDynamicShortcut(this, newShortcut);
+ ps.addOrUpdateDynamicShortcut(newShortcut);
}
+
+ // Lastly, adjust the ranks.
+ ps.adjustRanks();
}
packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
+
return true;
}
@@ -1403,32 +1544,72 @@
synchronized (mLock) {
final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+ ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+
+ ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE);
+
// Throttling.
- if (!ps.tryApiCall(this)) {
+ if (!ps.tryApiCall()) {
return false;
}
+ // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
+ ps.clearAllImplicitRanks();
+ assignImplicitRanks(newShortcuts);
+
for (int i = 0; i < size; i++) {
final ShortcutInfo source = newShortcuts.get(i);
fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
final ShortcutInfo target = ps.findShortcutById(source.getId());
- if (target != null) {
- final boolean replacingIcon = (source.getIcon() != null);
- if (replacingIcon) {
- removeIcon(userId, target);
- }
+ if (target == null) {
+ continue;
+ }
- target.copyNonNullFieldsFrom(source);
+ if (target.isEnabled() != source.isEnabled()) {
+ Slog.w(TAG,
+ "ShortcutInfo.enabled cannot be changed with updateShortcuts()");
+ }
- if (replacingIcon) {
- saveIconAndFixUpShortcut(userId, target);
- }
+ // When updating the rank, we need to insert between existing ranks, so set
+ // this setRankChanged, and also copy the implicit rank fo adjustRanks().
+ if (source.hasRank()) {
+ target.setRankChanged();
+ target.setImplicitRank(source.getImplicitRank());
+ }
+
+ final boolean replacingIcon = (source.getIcon() != null);
+ if (replacingIcon) {
+ removeIcon(userId, target);
+ }
+
+ if (source.getActivity() != null &&
+ !source.getActivity().equals(target.getActivity())) {
+ // TODO When activity is changing, check the dynamic count.
+ }
+
+ // Note copyNonNullFieldsFrom() does the "updatable with?" check too.
+ target.copyNonNullFieldsFrom(source);
+ target.setTimestamp(injectCurrentTimeMillis());
+
+ if (replacingIcon) {
+ saveIconAndFixUpShortcut(userId, target);
+ }
+
+ // When we're updating any resource related fields, re-extract the res names and
+ // the values.
+ if (replacingIcon || source.hasStringResources()) {
+ fixUpShortcutResourceNamesAndValues(target);
}
}
+
+ // Lastly, adjust the ranks.
+ ps.adjustRanks();
}
packageShortcutsChanged(packageName, userId);
+ verifyStates();
+
return true;
}
@@ -1443,8 +1624,16 @@
synchronized (mLock) {
final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+ ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+
+ ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD);
+
+ // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
+ ps.clearAllImplicitRanks();
+ assignImplicitRanks(newShortcuts);
+
// Throttling.
- if (!ps.tryApiCall(this)) {
+ if (!ps.tryApiCall()) {
return false;
}
for (int i = 0; i < size; i++) {
@@ -1453,13 +1642,66 @@
// Validate the shortcut.
fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
+ // When ranks are changing, we need to insert between ranks, so set the
+ // "rank changed" flag.
+ newShortcut.setRankChanged();
+
// Add it.
- ps.addDynamicShortcut(this, newShortcut);
+ ps.addOrUpdateDynamicShortcut(newShortcut);
+ }
+
+ // Lastly, adjust the ranks.
+ ps.adjustRanks();
+ }
+ packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
+
+ return true;
+ }
+
+ @Override
+ public void disableShortcuts(String packageName, List shortcutIds,
+ String disabledMessage, int disabledMessageResId, @UserIdInt int userId) {
+ verifyCaller(packageName, userId);
+ Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided");
+
+ synchronized (mLock) {
+ final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+
+ ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds);
+
+ for (int i = shortcutIds.size() - 1; i >= 0; i--) {
+ ps.disableWithId(Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)),
+ disabledMessage, disabledMessageResId,
+ /* overrideImmutable=*/ false);
+ }
+
+ // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks.
+ ps.adjustRanks();
+ }
+ packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
+ }
+
+ @Override
+ public void enableShortcuts(String packageName, List shortcutIds, @UserIdInt int userId) {
+ verifyCaller(packageName, userId);
+ Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided");
+
+ synchronized (mLock) {
+ final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+
+ ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds);
+
+ for (int i = shortcutIds.size() - 1; i >= 0; i--) {
+ ps.enableWithId((String) shortcutIds.get(i));
}
}
packageShortcutsChanged(packageName, userId);
- return true;
+ verifyStates();
}
@Override
@@ -1469,12 +1711,21 @@
Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided");
synchronized (mLock) {
+ final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+
+ ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds);
+
for (int i = shortcutIds.size() - 1; i >= 0; i--) {
- getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this,
+ ps.deleteDynamicWithId(
Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)));
}
+
+ // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks.
+ ps.adjustRanks();
}
packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
}
@Override
@@ -1482,9 +1733,11 @@
verifyCaller(packageName, userId);
synchronized (mLock) {
- getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
+ getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts();
}
packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
}
@Override
@@ -1499,6 +1752,17 @@
}
@Override
+ public ParceledListSlice<ShortcutInfo> getManifestShortcuts(String packageName,
+ @UserIdInt int userId) {
+ verifyCaller(packageName, userId);
+ synchronized (mLock) {
+ return getShortcutsWithQueryLocked(
+ packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
+ ShortcutInfo::isManifestShortcut);
+ }
+ }
+
+ @Override
public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
@UserIdInt int userId) {
verifyCaller(packageName, userId);
@@ -1514,7 +1778,7 @@
final ArrayList<ShortcutInfo> ret = new ArrayList<>();
- getPackageShortcutsLocked(packageName, userId).findAll(this, ret, query, cloneFlags);
+ getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags);
return new ParceledListSlice<>(ret);
}
@@ -1533,7 +1797,7 @@
synchronized (mLock) {
return mMaxUpdatesPerInterval
- - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
+ - getPackageShortcutsLocked(packageName, userId).getApiCallCount();
}
}
@@ -1547,7 +1811,7 @@
}
@Override
- public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
+ public int getIconMaxDimensions(String packageName, int userId) {
verifyCaller(packageName, userId);
synchronized (mLock) {
@@ -1555,6 +1819,34 @@
}
}
+ @Override
+ public void reportShortcutUsed(String packageName, String shortcutId, int userId) {
+ verifyCaller(packageName, userId);
+
+ Preconditions.checkNotNull(shortcutId);
+
+ if (DEBUG) {
+ Slog.d(TAG, String.format("reportShortcutUsed: Shortcut %s package %s used on user %d",
+ shortcutId, packageName, userId));
+ }
+
+ synchronized (mLock) {
+ final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+ if (ps.findShortcutById(shortcutId) == null) {
+ Log.w(TAG, String.format("reportShortcutUsed: package %s doesn't have shortcut %s",
+ packageName, shortcutId));
+ return;
+ }
+ }
+
+ final long token = injectClearCallingIdentity();
+ try {
+ mUsageStatsManagerInternal.reportShortcutUsage(packageName, shortcutId, userId);
+ } finally {
+ injectRestoreCallingIdentity(token);
+ }
+ }
+
/**
* Reset all throttling, for developer options and command line. Only system/shell can call it.
*/
@@ -1608,14 +1900,14 @@
@VisibleForTesting
boolean hasShortcutHostPermissionInner(@NonNull String callingPackage, int userId) {
synchronized (mLock) {
- final long start = System.currentTimeMillis();
+ final long start = injectElapsedRealtime();
final ShortcutUser user = getUserShortcutsLocked(userId);
final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
// Default launcher from package manager.
- final long startGetHomeActivitiesAsUser = System.currentTimeMillis();
+ final long startGetHomeActivitiesAsUser = injectElapsedRealtime();
final ComponentName defaultLauncher = injectPackageManagerInternal()
.getHomeActivitiesAsUser(allHomeCandidates, userId);
logDurationStat(Stats.GET_DEFAULT_HOME, startGetHomeActivitiesAsUser);
@@ -1627,7 +1919,7 @@
Slog.v(TAG, "Default launcher from PM: " + detected);
}
} else {
- detected = user.getLauncherComponent();
+ detected = user.getDefaultLauncherComponent();
// TODO: Make sure it's still enabled.
if (DEBUG) {
@@ -1667,7 +1959,7 @@
if (DEBUG) {
Slog.v(TAG, "Detected launcher: " + detected);
}
- user.setLauncherComponent(this, detected);
+ user.setDefaultLauncherComponent(detected);
return detected.getPackageName().equals(callingPackage);
} else {
// Default launcher not found.
@@ -1701,7 +1993,7 @@
// First, remove the package from the package list (if the package is a publisher).
if (packageUserId == owningUserId) {
- if (user.removePackage(this, packageName) != null) {
+ if (user.removePackage(packageName) != null) {
doNotify = true;
}
}
@@ -1714,7 +2006,7 @@
// Now there may be orphan shortcuts because we removed pinned shortcuts at the previous
// step. Remove them too.
- user.forAllPackages(p -> p.refreshPinnedFlags(this));
+ user.forAllPackages(p -> p.refreshPinnedFlags());
scheduleSaveUser(owningUserId);
@@ -1740,17 +2032,17 @@
@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;
+ final boolean cloneKeyFieldOnly =
+ ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) != 0);
+ final int cloneFlag = cloneKeyFieldOnly ? ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO
+ : ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER;
if (packageName == null) {
shortcutIds = null; // LauncherAppsService already threw for it though.
}
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
if (packageName != null) {
getShortcutsInnerLocked(launcherUserId,
@@ -1775,7 +2067,13 @@
final ArraySet<String> ids = shortcutIds == null ? null
: new ArraySet<>(shortcutIds);
- getPackageShortcutsLocked(packageName, userId).findAll(ShortcutService.this, ret,
+ final ShortcutPackage p = getUserShortcutsLocked(userId)
+ .getPackageShortcutsIfExists(packageName);
+ if (p == null) {
+ return; // No need to instantiate ShortcutPackage.
+ }
+
+ p.findAll(ret,
(ShortcutInfo si) -> {
if (si.getLastChangedTimestamp() < changedSince) {
return false;
@@ -1783,17 +2081,25 @@
if (ids != null && !ids.contains(si.getId())) {
return false;
}
- if (componentName != null
- && !componentName.equals(si.getActivityComponent())) {
- return false;
+ if (componentName != null) {
+ if (si.getActivity() != null
+ && !si.getActivity().equals(componentName)) {
+ 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;
+ if (((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
+ && si.isDynamic()) {
+ return true;
+ }
+ if (((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
+ && si.isPinned()) {
+ return true;
+ }
+ if (((queryFlags & ShortcutQuery.FLAG_GET_MANIFEST) != 0)
+ && si.isManifestShortcut()) {
+ return true;
+ }
+ return false;
}, cloneFlag, callingPackage, launcherUserId);
}
@@ -1805,7 +2111,7 @@
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
final ShortcutInfo si = getShortcutInfoLocked(
launcherUserId, callingPackage, packageName, shortcutId, userId);
@@ -1819,9 +2125,14 @@
Preconditions.checkStringNotEmpty(packageName, "packageName");
Preconditions.checkStringNotEmpty(shortcutId, "shortcutId");
+ final ShortcutPackage p = getUserShortcutsLocked(userId)
+ .getPackageShortcutsIfExists(packageName);
+ if (p == null) {
+ return null;
+ }
+
final ArrayList<ShortcutInfo> list = new ArrayList<>(1);
- getPackageShortcutsLocked(packageName, userId).findAll(
- ShortcutService.this, list,
+ p.findAll(list,
(ShortcutInfo si) -> shortcutId.equals(si.getId()),
/* clone flags=*/ 0, callingPackage, launcherUserId);
return list.size() == 0 ? null : list.get(0);
@@ -1838,12 +2149,13 @@
synchronized (mLock) {
final ShortcutLauncher launcher =
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId);
- launcher.attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ launcher.attemptToRestoreIfNeededAndSave();
- launcher.pinShortcuts(
- ShortcutService.this, userId, packageName, shortcutIds);
+ launcher.pinShortcuts(userId, packageName, shortcutIds);
}
packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
}
@Override
@@ -1856,13 +2168,13 @@
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
// Make sure the shortcut is actually visible to the launcher.
final ShortcutInfo si = getShortcutInfoLocked(
launcherUserId, callingPackage, packageName, shortcutId, userId);
// "si == null" should suffice here, but check the flags too just to make sure.
- if (si == null || !(si.isDynamic() || si.isPinned())) {
+ if (si == null || !si.isEnabled() || !si.isAlive()) {
return null;
}
return si.getIntent();
@@ -1885,10 +2197,15 @@
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
- final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
- packageName, userId).findShortcutById(shortcutId);
+ final ShortcutPackage p = getUserShortcutsLocked(userId)
+ .getPackageShortcutsIfExists(packageName);
+ if (p == null) {
+ return 0;
+ }
+
+ final ShortcutInfo shortcutInfo = p.findShortcutById(shortcutId);
return (shortcutInfo != null && shortcutInfo.hasIconResource())
? shortcutInfo.getIconResourceId() : 0;
}
@@ -1904,10 +2221,15 @@
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
- final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
- packageName, userId).findShortcutById(shortcutId);
+ final ShortcutPackage p = getUserShortcutsLocked(userId)
+ .getPackageShortcutsIfExists(packageName);
+ if (p == null) {
+ return null;
+ }
+
+ final ShortcutInfo shortcutInfo = p.findShortcutById(shortcutId);
if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
return null;
}
@@ -1938,9 +2260,6 @@
*/
@Override
public void onSystemLocaleChangedNoLock() {
- if (!FEATURE_ENABLED) {
- return;
- }
// DO NOT HOLD ANY LOCKS HERE.
// We want to reset throttling for all packages for all users. But we can't just do so
@@ -1953,8 +2272,29 @@
//
// This allows ShortcutUser's to detect the system locale change, so they can reset
// counters.
- mLocaleChangeSequenceNumber.incrementAndGet();
- postToHandler(() -> scheduleSaveBaseState());
+
+ // Ignore all callback during system boot.
+ if (mBootCompleted.get()) {
+ mLocaleChangeSequenceNumber.incrementAndGet();
+ if (DEBUG) {
+ Slog.d(TAG, "onSystemLocaleChangedNoLock: " + mLocaleChangeSequenceNumber.get());
+ }
+ injectPostToHandler(() -> handleLocaleChanged());
+ }
+ }
+ }
+
+ void handleLocaleChanged() {
+ if (DEBUG) {
+ Slog.d(TAG, "handleLocaleChanged");
+ }
+ scheduleSaveBaseState();
+
+ final long token = injectClearCallingIdentity();
+ try {
+ forEachLoadedUserLocked(u -> u.forAllPackages(p -> p.resolveResourceStrings()));
+ } finally {
+ injectRestoreCallingIdentity(token);
}
}
@@ -1995,32 +2335,45 @@
if (DEBUG) {
Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId);
}
- final ArrayList<PackageWithUser> gonePackages = new ArrayList<>();
- synchronized (mLock) {
- final ShortcutUser user = getUserShortcutsLocked(ownerUserId);
+ final long start = injectElapsedRealtime();
+ try {
+ final ArrayList<PackageWithUser> gonePackages = new ArrayList<>();
- user.forAllPackageItems(spi -> {
- if (spi.getPackageInfo().isShadow()) {
- return; // Don't delete shadow information.
+ synchronized (mLock) {
+ final ShortcutUser user = getUserShortcutsLocked(ownerUserId);
+
+ // Find packages that have been uninstalled.
+ user.forAllPackageItems(spi -> {
+ if (spi.getPackageInfo().isShadow()) {
+ return; // Don't delete shadow information.
+ }
+ if (!isPackageInstalled(spi.getPackageName(), spi.getPackageUserId())) {
+ gonePackages.add(PackageWithUser.of(spi));
+ }
+ });
+ if (gonePackages.size() > 0) {
+ for (int i = gonePackages.size() - 1; i >= 0; i--) {
+ final PackageWithUser pu = gonePackages.get(i);
+ cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId);
+ }
}
- final int versionCode = getApplicationVersionCode(
- spi.getPackageName(), spi.getPackageUserId());
- if (versionCode >= 0) {
- // Package still installed, see if it's updated.
- getUserShortcutsLocked(ownerUserId).handlePackageUpdated(
- this, spi.getPackageName(), versionCode);
- } else {
- gonePackages.add(PackageWithUser.of(spi));
- }
- });
- if (gonePackages.size() > 0) {
- for (int i = gonePackages.size() - 1; i >= 0; i--) {
- final PackageWithUser pu = gonePackages.get(i);
- cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId);
- }
+ final long now = injectCurrentTimeMillis();
+
+ // Then for each installed app, publish manifest shortcuts when needed.
+ forUpdatedPackages(ownerUserId, user.getLastAppScanTime(), ai -> {
+ user.handlePackageAddedOrUpdated(ai.packageName);
+ });
+
+ // Write the time just before the scan, because there may be apps that have just
+ // been updated, and we want to catch them in the next time.
+ user.setLastAppScanTime(now);
+ scheduleSaveUser(ownerUserId);
}
+ } finally {
+ logDurationStat(Stats.CHECK_PACKAGE_CHANGES, start);
}
+ verifyStates();
}
private void handlePackageAdded(String packageName, @UserIdInt int userId) {
@@ -2028,9 +2381,11 @@
Slog.d(TAG, String.format("handlePackageAdded: %s user=%d", packageName, userId));
}
synchronized (mLock) {
- forEachLoadedUserLocked(user ->
- user.attemptToRestoreIfNeededAndSave(this, packageName, userId));
+ final ShortcutUser user = getUserShortcutsLocked(userId);
+ user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
+ user.handlePackageAddedOrUpdated(packageName);
}
+ verifyStates();
}
private void handlePackageUpdateFinished(String packageName, @UserIdInt int userId) {
@@ -2039,15 +2394,14 @@
packageName, userId));
}
synchronized (mLock) {
- forEachLoadedUserLocked(user ->
- user.attemptToRestoreIfNeededAndSave(this, packageName, userId));
+ final ShortcutUser user = getUserShortcutsLocked(userId);
+ user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
- final int versionCode = getApplicationVersionCode(packageName, userId);
- if (versionCode < 0) {
- return; // shouldn't happen
+ if (isPackageInstalled(packageName, userId)) {
+ user.handlePackageAddedOrUpdated(packageName);
}
- getUserShortcutsLocked(userId).handlePackageUpdated(this, packageName, versionCode);
}
+ verifyStates();
}
private void handlePackageRemoved(String packageName, @UserIdInt int packageUserId) {
@@ -2056,6 +2410,8 @@
packageUserId));
}
cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+
+ verifyStates();
}
private void handlePackageDataCleared(String packageName, int packageUserId) {
@@ -2064,14 +2420,22 @@
packageUserId));
}
cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+
+ verifyStates();
}
// === PackageManager interaction ===
+ @Nullable
PackageInfo getPackageInfoWithSignatures(String packageName, @UserIdInt int userId) {
return injectPackageInfo(packageName, userId, true);
}
+ @Nullable
+ PackageInfo getPackageInfo(String packageName, @UserIdInt int userId) {
+ return injectPackageInfo(packageName, userId, false);
+ }
+
int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
final long token = injectClearCallingIdentity();
try {
@@ -2086,10 +2450,11 @@
}
}
+ @Nullable
@VisibleForTesting
PackageInfo injectPackageInfo(String packageName, @UserIdInt int userId,
boolean getSignatures) {
- final long start = System.currentTimeMillis();
+ final long start = injectElapsedRealtime();
final long token = injectClearCallingIdentity();
try {
return mIPackageManager.getPackageInfo(packageName, PACKAGE_MATCH_FLAGS
@@ -2108,9 +2473,10 @@
}
}
+ @Nullable
@VisibleForTesting
ApplicationInfo injectApplicationInfo(String packageName, @UserIdInt int userId) {
- final long start = System.currentTimeMillis();
+ final long start = injectElapsedRealtime();
final long token = injectClearCallingIdentity();
try {
return mIPackageManager.getApplicationInfo(packageName, PACKAGE_MATCH_FLAGS, userId);
@@ -2125,6 +2491,68 @@
}
}
+ @Nullable
+ @VisibleForTesting
+ PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ return mIPackageManager.getPackageInfo(packageName,
+ PACKAGE_MATCH_FLAGS | PackageManager.GET_ACTIVITIES
+ | PackageManager.GET_META_DATA, userId);
+ } catch (RemoteException e) {
+ // Shouldn't happen.
+ Slog.wtf(TAG, "RemoteException", e);
+ return null;
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.GET_ACTIVITIES_WITH_METADATA, start);
+ }
+ }
+
+ @Nullable
+ @VisibleForTesting
+ List<PackageInfo> injectInstalledPackages(@UserIdInt int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ final ParceledListSlice<PackageInfo> parceledList =
+ mIPackageManager.getInstalledPackages(PACKAGE_MATCH_FLAGS, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ // Shouldn't happen.
+ Slog.wtf(TAG, "RemoteException", e);
+ return null;
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.GET_INSTALLED_PACKAGES, start);
+ }
+ }
+
+ private void forUpdatedPackages(@UserIdInt int userId, long lastScanTime,
+ Consumer<ApplicationInfo> callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "forUpdatedPackages for user " + userId + ", lastScanTime=" + lastScanTime);
+ }
+ final List<PackageInfo> list = injectInstalledPackages(userId);
+ for (int i = list.size() - 1; i >= 0; i--) {
+ final PackageInfo pi = list.get(i);
+
+ if (((pi.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
+ && (pi.lastUpdateTime >= lastScanTime)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Found updated package " + pi.packageName);
+ }
+ callback.accept(pi.applicationInfo);
+ }
+ }
+ }
+
private boolean isApplicationFlagSet(String packageName, int userId, int flags) {
final ApplicationInfo ai = injectApplicationInfo(packageName, userId);
return (ai != null) && ((ai.flags & flags) == flags);
@@ -2134,15 +2562,26 @@
return isApplicationFlagSet(packageName, userId, ApplicationInfo.FLAG_INSTALLED);
}
- /**
- * @return the version code of the package, or -1 if the app is not installed.
- */
- int getApplicationVersionCode(String packageName, int userId) {
- final ApplicationInfo ai = injectApplicationInfo(packageName, userId);
- if ((ai == null) || ((ai.flags & ApplicationInfo.FLAG_INSTALLED) == 0)) {
- return -1;
+ @Nullable
+ XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
+ return activityInfo.loadXmlMetaData(mContext.getPackageManager(), key);
+ }
+
+ @Nullable
+ Resources injectGetResourcesForApplicationAsUser(String packageName, int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ return mContext.getPackageManager().getResourcesForApplicationAsUser(
+ packageName, userId);
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "Resources for package " + packageName + " not found");
+ return null;
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.GET_APPLICATION_RESOURCES, start);
}
- return ai.versionCode;
}
// === Backup & restore ===
@@ -2168,7 +2607,7 @@
return null;
}
- user.forAllPackageItems(spi -> spi.refreshPackageInfoAndSave(this));
+ user.forAllPackageItems(spi -> spi.refreshPackageInfoAndSave());
// Then save.
final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024);
@@ -2283,11 +2722,17 @@
dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO, "getPackageInfo()");
dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO_WITH_SIG, "getPackageInfo(SIG)");
dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo");
+ dumpStatLS(pw, p, Stats.CLEANUP_DANGLING_BITMAPS, "cleanupDanglingBitmaps");
+ dumpStatLS(pw, p, Stats.GET_ACTIVITIES_WITH_METADATA, "getActivities+metadata");
+ dumpStatLS(pw, p, Stats.GET_INSTALLED_PACKAGES, "getInstalledPackages");
+ dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges");
+ dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources");
+ dumpStatLS(pw, p, Stats.RESOURCE_NAME_LOOKUP, "resourceNameLookup");
}
for (int i = 0; i < mUsers.size(); i++) {
pw.println();
- mUsers.valueAt(i).dump(this, pw, " ");
+ mUsers.valueAt(i).dump(pw, " ");
}
pw.println();
@@ -2499,8 +2944,7 @@
private void clearLauncher() {
synchronized (mLock) {
- getUserShortcutsLocked(mUserId).setLauncherComponent(
- ShortcutService.this, null);
+ getUserShortcutsLocked(mUserId).setDefaultLauncherComponent(null);
}
}
@@ -2510,7 +2954,7 @@
hasShortcutHostPermissionInner("-", mUserId);
getOutPrintWriter().println("Launcher: "
- + getUserShortcutsLocked(mUserId).getLauncherComponent());
+ + getUserShortcutsLocked(mUserId).getDefaultLauncherComponent());
}
}
@@ -2663,15 +3107,47 @@
}
@VisibleForTesting
- ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) {
+ ShortcutPackage getPackageShortcutForTest(String packageName, int userId) {
synchronized (mLock) {
final ShortcutUser user = mUsers.get(userId);
if (user == null) return null;
- final ShortcutPackage pkg = user.getAllPackagesForTest().get(packageName);
+ return user.getAllPackagesForTest().get(packageName);
+ }
+ }
+
+ @VisibleForTesting
+ ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) {
+ synchronized (mLock) {
+ final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId);
if (pkg == null) return null;
return pkg.findShortcutById(shortcutId);
}
}
+
+ /**
+ * Control whether {@link #verifyStates} should be performed. We always perform it during unit
+ * tests.
+ */
+ @VisibleForTesting
+ boolean injectShouldPerformVerification() {
+ return DEBUG;
+ }
+
+ /**
+ * Check various internal states and throws if there's any inconsistency.
+ * This is normally only enabled during unit tests.
+ */
+ final void verifyStates() {
+ if (injectShouldPerformVerification()) {
+ verifyStatesInner();
+ }
+ }
+
+ private void verifyStatesInner() {
+ synchronized (this) {
+ forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));
+ }
+ }
}
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 7d19a78..f8ee325 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -16,6 +16,7 @@
package com.android.server.pm;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.text.format.Formatter;
@@ -23,6 +24,7 @@
import android.util.Slog;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
@@ -39,6 +41,8 @@
/**
* User information used by {@link ShortcutService}.
+ *
+ * All methods should be guarded by {@code #mService.mLock}.
*/
class ShortcutUser {
private static final String TAG = ShortcutService.TAG;
@@ -48,6 +52,7 @@
private static final String ATTR_VALUE = "value";
private static final String ATTR_KNOWN_LOCALE_CHANGE_SEQUENCE_NUMBER = "locale-seq-no";
+ private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time";
static final class PackageWithUser {
final int userId;
@@ -87,6 +92,8 @@
}
}
+ final ShortcutService mService;
+
@UserIdInt
private final int mUserId;
@@ -97,11 +104,14 @@
private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
/** Default launcher that can access the launcher apps APIs. */
- private ComponentName mLauncherComponent;
+ private ComponentName mDefaultLauncherComponent;
private long mKnownLocaleChangeSequenceNumber;
- public ShortcutUser(int userId) {
+ private long mLastAppScanTime;
+
+ public ShortcutUser(ShortcutService service, int userId) {
+ mService = service;
mUserId = userId;
}
@@ -109,6 +119,14 @@
return mUserId;
}
+ public long getLastAppScanTime() {
+ return mLastAppScanTime;
+ }
+
+ public void setLastAppScanTime(long lastAppScanTime) {
+ mLastAppScanTime = lastAppScanTime;
+ }
+
// We don't expose this directly to non-test code because only ShortcutUser should add to/
// remove from it.
@VisibleForTesting
@@ -116,10 +134,14 @@
return mPackages;
}
- public ShortcutPackage removePackage(@NonNull ShortcutService s, @NonNull String packageName) {
+ public boolean hasPackage(@NonNull String packageName) {
+ return mPackages.containsKey(packageName);
+ }
+
+ public ShortcutPackage removePackage(@NonNull String packageName) {
final ShortcutPackage removed = mPackages.remove(packageName);
- s.cleanupBitmapsForPackage(mUserId, packageName);
+ mService.cleanupBitmapsForPackage(mUserId, packageName);
return removed;
}
@@ -136,23 +158,33 @@
launcher.getPackageName()), launcher);
}
+ @Nullable
public ShortcutLauncher removeLauncher(
@UserIdInt int packageUserId, @NonNull String packageName) {
return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
}
- public ShortcutPackage getPackageShortcuts(ShortcutService s, @NonNull String packageName) {
- ShortcutPackage ret = mPackages.get(packageName);
- if (ret == null) {
- ret = new ShortcutPackage(s, this, mUserId, packageName);
- mPackages.put(packageName, ret);
- } else {
- ret.attemptToRestoreIfNeededAndSave(s);
+ @Nullable
+ public ShortcutPackage getPackageShortcutsIfExists(@NonNull String packageName) {
+ final ShortcutPackage ret = mPackages.get(packageName);
+ if (ret != null) {
+ ret.attemptToRestoreIfNeededAndSave();
}
return ret;
}
- public ShortcutLauncher getLauncherShortcuts(ShortcutService s, @NonNull String packageName,
+ @NonNull
+ public ShortcutPackage getPackageShortcuts(@NonNull String packageName) {
+ ShortcutPackage ret = getPackageShortcutsIfExists(packageName);
+ if (ret == null) {
+ ret = new ShortcutPackage(this, mUserId, packageName);
+ mPackages.put(packageName, ret);
+ }
+ return ret;
+ }
+
+ @NonNull
+ public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
@UserIdInt int launcherUserId) {
final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
ShortcutLauncher ret = mLaunchers.get(key);
@@ -160,7 +192,7 @@
ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
mLaunchers.put(key, ret);
} else {
- ret.attemptToRestoreIfNeededAndSave(s);
+ ret.attemptToRestoreIfNeededAndSave();
}
return ret;
}
@@ -197,8 +229,8 @@
/**
* Reset all throttling counters for all packages, if there has been a system locale change.
*/
- public void resetThrottlingIfNeeded(ShortcutService s) {
- final long currentNo = s.getLocaleChangeSequenceNumber();
+ public void resetThrottlingIfNeeded() {
+ final long currentNo = mService.getLocaleChangeSequenceNumber();
if (mKnownLocaleChangeSequenceNumber < currentNo) {
if (ShortcutService.DEBUG) {
Slog.d(TAG, "LocaleChange detected for user " + mUserId);
@@ -206,61 +238,64 @@
mKnownLocaleChangeSequenceNumber = currentNo;
- forAllPackages(p -> p.resetRateLimiting(s));
+ forAllPackages(p -> p.resetRateLimiting());
- s.scheduleSaveUser(mUserId);
+ mService.scheduleSaveUser(mUserId);
}
}
- /**
- * Called when a package is updated.
- */
- public void handlePackageUpdated(ShortcutService s, @NonNull String packageName,
- int newVersionCode) {
- if (!mPackages.containsKey(packageName)) {
- return;
+ public void handlePackageAddedOrUpdated(@NonNull String packageName) {
+ final boolean isNewApp = !mPackages.containsKey(packageName);
+
+ final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
+
+ if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp)) {
+ if (isNewApp) {
+ mPackages.remove(packageName);
+ }
}
- getPackageShortcuts(s, packageName).handlePackageUpdated(s, newVersionCode);
}
public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName,
@UserIdInt int packageUserId) {
forPackageItem(packageName, packageUserId, spi -> {
- spi.attemptToRestoreIfNeededAndSave(s);
+ spi.attemptToRestoreIfNeededAndSave();
});
}
- public void saveToXml(ShortcutService s, XmlSerializer out, boolean forBackup)
+ public void saveToXml(XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException {
out.startTag(null, TAG_ROOT);
ShortcutService.writeAttr(out, ATTR_KNOWN_LOCALE_CHANGE_SEQUENCE_NUMBER,
mKnownLocaleChangeSequenceNumber);
+ ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_TIME,
+ mLastAppScanTime);
ShortcutService.writeTagValue(out, TAG_LAUNCHER,
- mLauncherComponent);
+ mDefaultLauncherComponent);
// Can't use forEachPackageItem due to the checked exceptions.
{
final int size = mLaunchers.size();
for (int i = 0; i < size; i++) {
- saveShortcutPackageItem(s, out, mLaunchers.valueAt(i), forBackup);
+ saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup);
}
}
{
final int size = mPackages.size();
for (int i = 0; i < size; i++) {
- saveShortcutPackageItem(s, out, mPackages.valueAt(i), forBackup);
+ saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup);
}
}
out.endTag(null, TAG_ROOT);
}
- private void saveShortcutPackageItem(ShortcutService s, XmlSerializer out,
+ private void saveShortcutPackageItem(XmlSerializer out,
ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException {
if (forBackup) {
- if (!s.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) {
+ if (!mService.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) {
return; // Don't save.
}
if (spi.getPackageUserId() != spi.getOwnerUserId()) {
@@ -272,11 +307,18 @@
public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
boolean fromBackup) throws IOException, XmlPullParserException {
- final ShortcutUser ret = new ShortcutUser(userId);
+ final ShortcutUser ret = new ShortcutUser(s, userId);
ret.mKnownLocaleChangeSequenceNumber = ShortcutService.parseLongAttribute(parser,
ATTR_KNOWN_LOCALE_CHANGE_SEQUENCE_NUMBER);
+ // If lastAppScanTime is in the future, that means the clock went backwards.
+ // Just scan all apps again.
+ final long lastAppScanTime = ShortcutService.parseLongAttribute(parser,
+ ATTR_LAST_APP_SCAN_TIME);
+ final long currentTime = s.injectCurrentTimeMillis();
+ ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0;
+
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -290,7 +332,7 @@
if (depth == outerDepth + 1) {
switch (tag) {
case TAG_LAUNCHER: {
- ret.mLauncherComponent = ShortcutService.parseComponentNameAttribute(
+ ret.mDefaultLauncherComponent = ShortcutService.parseComponentNameAttribute(
parser, ATTR_VALUE);
continue;
}
@@ -315,16 +357,16 @@
return ret;
}
- public ComponentName getLauncherComponent() {
- return mLauncherComponent;
+ public ComponentName getDefaultLauncherComponent() {
+ return mDefaultLauncherComponent;
}
- public void setLauncherComponent(ShortcutService s, ComponentName launcherComponent) {
- if (Objects.equal(mLauncherComponent, launcherComponent)) {
+ public void setDefaultLauncherComponent(ComponentName launcherComponent) {
+ if (Objects.equal(mDefaultLauncherComponent, launcherComponent)) {
return;
}
- mLauncherComponent = launcherComponent;
- s.scheduleSaveUser(mUserId);
+ mDefaultLauncherComponent = launcherComponent;
+ mService.scheduleSaveUser(mUserId);
}
public void resetThrottling() {
@@ -333,36 +375,38 @@
}
}
- public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+ public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.print(prefix);
pw.print("User: ");
pw.print(mUserId);
pw.print(" Known locale seq#: ");
pw.print(mKnownLocaleChangeSequenceNumber);
+ pw.print(" Last app scan: ");
+ pw.print(mLastAppScanTime);
pw.println();
prefix += prefix + " ";
pw.print(prefix);
pw.print("Default launcher: ");
- pw.print(mLauncherComponent);
+ pw.print(mDefaultLauncherComponent);
pw.println();
for (int i = 0; i < mLaunchers.size(); i++) {
- mLaunchers.valueAt(i).dump(s, pw, prefix);
+ mLaunchers.valueAt(i).dump(pw, prefix);
}
for (int i = 0; i < mPackages.size(); i++) {
- mPackages.valueAt(i).dump(s, pw, prefix);
+ mPackages.valueAt(i).dump(pw, prefix);
}
pw.println();
pw.print(prefix);
pw.println("Bitmap directories: ");
- dumpDirectorySize(s, pw, prefix + " ", s.getUserBitmapFilePath(mUserId));
+ dumpDirectorySize(pw, prefix + " ", mService.getUserBitmapFilePath(mUserId));
}
- private void dumpDirectorySize(@NonNull ShortcutService s, @NonNull PrintWriter pw,
+ private void dumpDirectorySize(@NonNull PrintWriter pw,
@NonNull String prefix, File path) {
int numFiles = 0;
long size = 0;
@@ -373,7 +417,7 @@
numFiles++;
size += child.length();
} else if (child.isDirectory()) {
- dumpDirectorySize(s, pw, prefix + " ", child);
+ dumpDirectorySize(pw, prefix + " ", child);
}
}
}
@@ -385,7 +429,7 @@
pw.print(" files, size=");
pw.print(size);
pw.print(" (");
- pw.print(Formatter.formatFileSize(s.mContext, size));
+ pw.print(Formatter.formatFileSize(mService.mContext, size));
pw.println(")");
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 4c515f0..ced44fa 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -27,11 +27,13 @@
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerNative;
+import android.app.AppGlobals;
import android.app.IActivityManager;
import android.app.IStopUserCallback;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -70,6 +72,7 @@
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.IntArray;
import android.util.Log;
@@ -452,6 +455,8 @@
// user restriction was not a default guest restriction.
setUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, true, currentGuestUser.id);
}
+
+ maybeInitializeDemoMode(UserHandle.USER_SYSTEM);
mContext.registerReceiver(mDisableQuietModeCallback,
new IntentFilter(ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK),
null, mHandler);
@@ -2162,6 +2167,7 @@
final boolean isGuest = (flags & UserInfo.FLAG_GUEST) != 0;
final boolean isManagedProfile = (flags & UserInfo.FLAG_MANAGED_PROFILE) != 0;
final boolean isRestricted = (flags & UserInfo.FLAG_RESTRICTED) != 0;
+ final boolean isDemo = (flags & UserInfo.FLAG_DEMO) != 0;
final long ident = Binder.clearCallingIdentity();
UserInfo userInfo;
UserData userData;
@@ -2179,8 +2185,8 @@
Log.e(LOG_TAG, "Cannot add more managed profiles for user " + parentId);
return null;
}
- if (!isGuest && !isManagedProfile && isUserLimitReached()) {
- // If we're not adding a guest user or a managed profile and the limit has
+ if (!isGuest && !isManagedProfile && !isDemo && isUserLimitReached()) {
+ // If we're not adding a guest/demo user or a managed profile and the limit has
// been reached, cannot add a user.
return null;
}
@@ -2206,7 +2212,8 @@
return null;
}
}
- if (!UserManager.isSplitSystemUser() && (flags & UserInfo.FLAG_EPHEMERAL) != 0) {
+ if (!UserManager.isSplitSystemUser() && (flags & UserInfo.FLAG_EPHEMERAL) != 0
+ && (flags & UserInfo.FLAG_DEMO) == 0) {
Log.e(LOG_TAG,
"Ephemeral users are supported on split-system-user systems only.");
return null;
@@ -2857,6 +2864,8 @@
mPm.onBeforeUserStartUninitialized(userId);
}
}
+
+ maybeInitializeDemoMode(userId);
}
/**
@@ -2871,6 +2880,7 @@
/**
* Make a note of the last started time of a user and do some cleanup.
+ * This is called with ActivityManagerService lock held.
* @param userId the user that was just foregrounded
*/
public void onUserLoggedIn(@UserIdInt int userId) {
@@ -2888,6 +2898,24 @@
scheduleWriteUser(userData);
}
+ private void maybeInitializeDemoMode(int userId) {
+ if (UserManager.isDeviceInDemoMode(mContext)) {
+ String demoLauncher =
+ mContext.getResources().getString(
+ com.android.internal.R.string.config_demoModeLauncherComponent);
+ if (!TextUtils.isEmpty(demoLauncher)) {
+ ComponentName componentToEnable = ComponentName.unflattenFromString(demoLauncher);
+ try {
+ AppGlobals.getPackageManager().setComponentEnabledSetting(componentToEnable,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ 0,
+ /* userId= */ userId);
+ } catch (RemoteException re) {
+ // Internal, shouldn't happen
+ }
+ }
+ }
+ }
+
/**
* Returns the next available user id, filling in any holes in the ids.
* TODO: May not be a good idea to recycle ids, in case it results in confusion
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 38a3f42..c082143 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -33,6 +33,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
+import android.service.persistentdata.PersistentDataBlockManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.util.Log;
@@ -424,6 +425,14 @@
android.provider.Settings.Global.SAFE_BOOT_DISALLOWED,
newValue ? 1 : 0);
break;
+ case UserManager.DISALLOW_FACTORY_RESET:
+ if (newValue) {
+ PersistentDataBlockManager manager = (PersistentDataBlockManager) context
+ .getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
+ if (manager != null && manager.getOemUnlockEnabled()) {
+ manager.setOemUnlockEnabled(false);
+ }
+ }
}
} finally {
Binder.restoreCallingIdentity(id);
diff --git a/services/core/java/com/android/server/policy/GlobalActions.java b/services/core/java/com/android/server/policy/GlobalActions.java
index 5ef518e..6fc15f0 100644
--- a/services/core/java/com/android/server/policy/GlobalActions.java
+++ b/services/core/java/com/android/server/policy/GlobalActions.java
@@ -1145,7 +1145,7 @@
public GlobalActionsDialog(Context context, AlertParams params) {
super(context, getDialogTheme(context));
mContext = getContext();
- mAlert = new AlertController(mContext, this, getWindow());
+ mAlert = AlertController.create(mContext, this, getWindow());
mAdapter = (MyAdapter) params.mAdapter;
mWindowTouchSlop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
params.apply(mAlert);
diff --git a/services/core/java/com/android/server/policy/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/policy/ImmersiveModeConfirmation.java
index a77d512..2e32fe3 100644
--- a/services/core/java/com/android/server/policy/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/policy/ImmersiveModeConfirmation.java
@@ -30,6 +30,7 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.service.vr.IVrManager;
import android.service.vr.IVrStateCallbacks;
@@ -142,7 +143,8 @@
if (!disabled
&& (DEBUG_SHOW_EVERY_TIME || !mConfirmed)
&& userSetupComplete
- && !mVrModeEnabled) {
+ && !mVrModeEnabled
+ && !UserManager.isDeviceInDemoMode(mContext)) {
mHandler.sendEmptyMessageDelayed(H.SHOW, mShowDelayMs);
}
} else {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index a4408fc..f9f0fd9 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -262,6 +262,10 @@
private static final String SYSUI_SCREENSHOT_ERROR_RECEIVER =
"com.android.systemui.screenshot.ScreenshotServiceErrorReceiver";
+ private static final int NAV_BAR_BOTTOM = 0;
+ private static final int NAV_BAR_RIGHT = 1;
+ private static final int NAV_BAR_LEFT = 2;
+
/**
* Keyguard stuff
*/
@@ -354,9 +358,8 @@
int mStatusBarHeight;
WindowState mNavigationBar = null;
boolean mHasNavigationBar = false;
- boolean mCanHideNavigationBar = false;
boolean mNavigationBarCanMove = false; // can the navigation bar ever move to the side?
- boolean mNavigationBarOnBottom = true; // is the navigation bar on the bottom *right now*?
+ int mNavigationBarPosition = NAV_BAR_BOTTOM;
int[] mNavigationBarHeightForRotationDefault = new int[4];
int[] mNavigationBarWidthForRotationDefault = new int[4];
int[] mNavigationBarHeightForRotationInCarMode = new int[4];
@@ -1683,13 +1686,19 @@
}
@Override
public void onSwipeFromBottom() {
- if (mNavigationBar != null && mNavigationBarOnBottom) {
+ if (mNavigationBar != null && mNavigationBarPosition == NAV_BAR_BOTTOM) {
requestTransientBars(mNavigationBar);
}
}
@Override
public void onSwipeFromRight() {
- if (mNavigationBar != null && !mNavigationBarOnBottom) {
+ if (mNavigationBar != null && mNavigationBarPosition == NAV_BAR_RIGHT) {
+ requestTransientBars(mNavigationBar);
+ }
+ }
+ @Override
+ public void onSwipeFromLeft() {
+ if (mNavigationBar != null && mNavigationBarPosition == NAV_BAR_LEFT) {
requestTransientBars(mNavigationBar);
}
}
@@ -2782,8 +2791,8 @@
if (win.getAttrs().windowAnimations != 0) {
return 0;
}
- // This can be on either the bottom or the right.
- if (mNavigationBarOnBottom) {
+ // This can be on either the bottom or the right or the left.
+ if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
if (transit == TRANSIT_EXIT
|| transit == TRANSIT_HIDE) {
return R.anim.dock_bottom_exit;
@@ -2791,7 +2800,7 @@
|| transit == TRANSIT_SHOW) {
return R.anim.dock_bottom_enter;
}
- } else {
+ } else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
if (transit == TRANSIT_EXIT
|| transit == TRANSIT_HIDE) {
return R.anim.dock_right_exit;
@@ -2799,6 +2808,14 @@
|| transit == TRANSIT_SHOW) {
return R.anim.dock_right_enter;
}
+ } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
+ if (transit == TRANSIT_EXIT
+ || transit == TRANSIT_HIDE) {
+ return R.anim.dock_left_exit;
+ } else if (transit == TRANSIT_ENTER
+ || transit == TRANSIT_SHOW) {
+ return R.anim.dock_left_enter;
+ }
}
} else if (win.getAttrs().type == TYPE_DOCK_DIVIDER) {
return selectDockedDividerAnimationLw(win, transit);
@@ -2827,10 +2844,12 @@
// If the divider is behind the navigation bar, don't animate.
final Rect frame = win.getFrameLw();
final boolean behindNavBar = mNavigationBar != null
- && ((mNavigationBarOnBottom
+ && ((mNavigationBarPosition == NAV_BAR_BOTTOM
&& frame.top + insets >= mNavigationBar.getFrameLw().top)
- || (!mNavigationBarOnBottom
- && frame.left + insets >= mNavigationBar.getFrameLw().left));
+ || (mNavigationBarPosition == NAV_BAR_RIGHT
+ && frame.left + insets >= mNavigationBar.getFrameLw().left)
+ || (mNavigationBarPosition == NAV_BAR_LEFT
+ && frame.right - insets <= mNavigationBar.getFrameLw().right));
final boolean landscape = frame.height() > frame.width();
final boolean offscreenLandscape = landscape && (frame.right - insets <= 0
|| frame.left + insets >= win.getDisplayFrameLw().right);
@@ -4023,7 +4042,7 @@
navVisible |= !canHideNavigationBar();
boolean updateSysUiVisibility = layoutNavigationBar(displayWidth, displayHeight,
- displayRotation, uiMode, overscanRight, overscanBottom, dcf, navVisible, navTranslucent,
+ displayRotation, uiMode, overscanLeft, overscanRight, overscanBottom, dcf, navVisible, navTranslucent,
navAllowedHidden, statusBarExpandedNotKeyguard);
if (DEBUG_LAYOUT) Slog.i(TAG, String.format("mDock rect: (%d,%d - %d,%d)",
mDockLeft, mDockTop, mDockRight, mDockBottom));
@@ -4102,8 +4121,8 @@
}
private boolean layoutNavigationBar(int displayWidth, int displayHeight, int displayRotation,
- int uiMode, int overscanRight, int overscanBottom, Rect dcf, boolean navVisible,
- boolean navTranslucent, boolean navAllowedHidden,
+ int uiMode, int overscanLeft, int overscanRight, int overscanBottom, Rect dcf,
+ boolean navVisible, boolean navTranslucent, boolean navAllowedHidden,
boolean statusBarExpandedNotKeyguard) {
if (mNavigationBar != null) {
boolean transientNavBarShowing = mNavigationBarController.isTransientShowing();
@@ -4111,8 +4130,9 @@
// size. We need to do this directly, instead of relying on
// it to bubble up from the nav bar, because this needs to
// change atomically with screen rotations.
- mNavigationBarOnBottom = isNavigationBarOnBottom(displayWidth, displayHeight);
- if (mNavigationBarOnBottom) {
+ mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight,
+ displayRotation);
+ if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
// It's a system nav bar or a portrait screen; nav bar goes on bottom.
int top = displayHeight - overscanBottom
- getNavigationBarHeight(displayRotation, uiMode);
@@ -4138,7 +4158,7 @@
// we can tell the app that it is covered by it.
mSystemBottom = mTmpNavigationFrame.top;
}
- } else {
+ } else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
// Landscape screen; nav bar goes to the right.
int left = displayWidth - overscanRight
- getNavigationBarWidth(displayRotation, uiMode);
@@ -4164,6 +4184,33 @@
// we can tell the app that it is covered by it.
mSystemRight = mTmpNavigationFrame.left;
}
+ } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
+ // Seascape screen; nav bar goes to the left.
+ int right = overscanLeft + getNavigationBarWidth(displayRotation, uiMode);
+ mTmpNavigationFrame.set(overscanLeft, 0, right, displayHeight);
+ mStableLeft = mStableFullscreenLeft = mTmpNavigationFrame.right;
+ if (transientNavBarShowing) {
+ mNavigationBarController.setBarShowingLw(true);
+ } else if (navVisible) {
+ mNavigationBarController.setBarShowingLw(true);
+ mDockLeft = mTmpNavigationFrame.right;
+ // TODO: not so sure about those:
+ mRestrictedScreenLeft = mRestrictedOverscanScreenLeft = mDockLeft;
+ mRestrictedScreenWidth = mDockRight - mRestrictedScreenLeft;
+ mRestrictedOverscanScreenWidth = mDockRight - mRestrictedOverscanScreenLeft;
+ } else {
+ // We currently want to hide the navigation UI - unless we expanded the status
+ // bar.
+ mNavigationBarController.setBarShowingLw(statusBarExpandedNotKeyguard);
+ }
+ if (navVisible && !navTranslucent && !navAllowedHidden
+ && !mNavigationBar.isAnimatingLw()
+ && !mNavigationBarController.wasRecentlyTranslucent()) {
+ // If the nav bar is currently requested to be visible,
+ // and not in the process of animating on or off, then
+ // we can tell the app that it is covered by it.
+ mSystemLeft = mTmpNavigationFrame.right;
+ }
}
// Make sure the content and current rectangles are updated to
// account for the restrictions from the navigation bar.
@@ -4184,8 +4231,15 @@
return false;
}
- private boolean isNavigationBarOnBottom(int displayWidth, int displayHeight) {
- return !mNavigationBarCanMove || displayWidth < displayHeight;
+ private int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
+ if (mNavigationBarCanMove && displayWidth > displayHeight) {
+ if (displayRotation == Surface.ROTATION_270) {
+ return NAV_BAR_LEFT;
+ } else {
+ return NAV_BAR_RIGHT;
+ }
+ }
+ return NAV_BAR_BOTTOM;
}
/** {@inheritDoc} */
@@ -4361,7 +4415,11 @@
if (mStatusBar != null && mFocusedWindow == mStatusBar && canReceiveInput(mStatusBar)) {
// The status bar forces the navigation bar while it's visible. Make sure the IME
// avoids the navigation bar in that case.
- pf.right = df.right = of.right = cf.right = vf.right = mStableRight;
+ if (mNavigationBarPosition == NAV_BAR_RIGHT) {
+ pf.right = df.right = of.right = cf.right = vf.right = mStableRight;
+ } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
+ pf.left = df.left = of.left = cf.left = vf.left = mStableLeft;
+ }
}
// IM dock windows always go to the bottom of the screen.
attrs.gravity = Gravity.BOTTOM;
@@ -4390,6 +4448,8 @@
} else {
vf.set(cf);
}
+ } else if (attrs.type == TYPE_WALLPAPER) {
+ layoutWallpaper(win, pf, df, of, cf);
} else if (win == mStatusBar) {
pf.left = df.left = of.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
@@ -4613,17 +4673,6 @@
+ mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mOverscanScreenTop
+ mOverscanScreenHeight;
- } else if (attrs.type == TYPE_WALLPAPER) {
- // The wallpaper also has Real Ultimate Power, but we want to tell
- // it about the overscan area.
- pf.left = df.left = mOverscanScreenLeft;
- pf.top = df.top = mOverscanScreenTop;
- pf.right = df.right = mOverscanScreenLeft + mOverscanScreenWidth;
- pf.bottom = df.bottom = mOverscanScreenTop + mOverscanScreenHeight;
- of.left = cf.left = mUnrestrictedScreenLeft;
- of.top = cf.top = mUnrestrictedScreenTop;
- of.right = cf.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
- of.bottom = cf.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
} else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
@@ -4816,6 +4865,20 @@
}
}
+ private void layoutWallpaper(WindowState win, Rect pf, Rect df, Rect of, Rect cf) {
+
+ // The wallpaper also has Real Ultimate Power, but we want to tell
+ // it about the overscan area.
+ pf.left = df.left = mOverscanScreenLeft;
+ pf.top = df.top = mOverscanScreenTop;
+ pf.right = df.right = mOverscanScreenLeft + mOverscanScreenWidth;
+ pf.bottom = df.bottom = mOverscanScreenTop + mOverscanScreenHeight;
+ of.left = cf.left = mUnrestrictedScreenLeft;
+ of.top = cf.top = mUnrestrictedScreenTop;
+ of.right = cf.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
+ of.bottom = cf.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
+ }
+
private void offsetInputMethodWindowLw(WindowState win) {
int top = Math.max(win.getDisplayFrameLw().top, win.getContentFrameLw().top);
top += win.getGivenContentInsetsLw().top;
@@ -4997,7 +5060,7 @@
// Keep track of the window if it's dimming but not necessarily fullscreen.
final boolean reallyVisible = win.isVisibleOrBehindKeyguardLw() && !win.isGoneForLayoutLw();
- if (mTopFullscreenOpaqueOrDimmingWindowState == null && reallyVisible
+ if (mTopFullscreenOpaqueOrDimmingWindowState == null && reallyVisible
&& win.isDimming() && StackId.normallyFullscreenWindows(stackId)) {
mTopFullscreenOpaqueOrDimmingWindowState = win;
}
@@ -5655,6 +5718,17 @@
break;
}
+ case KeyEvent.KEYCODE_FP_NAV_DOWN:
+ // fall through
+ case KeyEvent.KEYCODE_FP_NAV_UP:
+ // fall through
+ case KeyEvent.KEYCODE_FP_NAV_LEFT:
+ // fall through
+ case KeyEvent.KEYCODE_FP_NAV_RIGHT: {
+ interceptStatusBarKey(event);
+ break;
+ }
+
case KeyEvent.KEYCODE_SLEEP: {
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false;
@@ -5776,6 +5850,32 @@
}
/**
+ * Handle statusbar expansion events.
+ * @param event
+ */
+ private void interceptStatusBarKey(KeyEvent event) {
+ final int e = event.getKeyCode();
+ if (event.getAction() == KeyEvent.ACTION_UP && areSystemNavigationKeysEnabled()) {
+ boolean doOpen = false;
+ boolean doClose = false;
+ doOpen = (e == KeyEvent.KEYCODE_FP_NAV_DOWN);
+ doClose = (e == KeyEvent.KEYCODE_FP_NAV_UP);
+ IStatusBarService sbar = getStatusBarService();
+ if (sbar != null) {
+ try {
+ if (doOpen) {
+ sbar.expandNotificationsPanel();
+ } else if (doClose) {
+ sbar.collapsePanels();
+ }
+ } catch (RemoteException e1) {
+ // oops, no statusbar. Ignore event.
+ }
+ }
+ }
+ }
+
+ /**
* Returns true if the key can have global actions attached to it.
* We reserve all power management keys for the system since they require
* very careful handling.
@@ -6425,10 +6525,13 @@
// Only navigation bar
if (mNavigationBar != null) {
- if (isNavigationBarOnBottom(displayWidth, displayHeight)) {
+ int position = navigationBarPosition(displayWidth, displayHeight, displayRotation);
+ if (position == NAV_BAR_BOTTOM) {
outInsets.bottom = getNavigationBarHeight(displayRotation, mUiMode);
- } else {
+ } else if (position == NAV_BAR_RIGHT) {
outInsets.right = getNavigationBarWidth(displayRotation, mUiMode);
+ } else if (position == NAV_BAR_LEFT) {
+ outInsets.left = getNavigationBarWidth(displayRotation, mUiMode);
}
}
}
@@ -6774,9 +6877,7 @@
@Override public void run() {
if (mBootMsgDialog == null) {
int theme;
- if (mHasFeatureWatch) {
- theme = com.android.internal.R.style.Theme_Micro_Dialog_Alert;
- } else if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEVISION)) {
+ if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEVISION)) {
theme = com.android.internal.R.style.Theme_Leanback_Dialog_Alert;
} else {
theme = 0;
@@ -7165,6 +7266,11 @@
Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1;
}
+ private boolean areSystemNavigationKeysEnabled() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SYSTEM_NAVIGATION_KEYS_ENABLED, 0) == 1;
+ }
+
@Override
public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always) {
if (!mVibrator.hasVibrator()) {
diff --git a/services/core/java/com/android/server/policy/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/policy/SystemGesturesPointerEventListener.java
index 80e4341..598c58e 100644
--- a/services/core/java/com/android/server/policy/SystemGesturesPointerEventListener.java
+++ b/services/core/java/com/android/server/policy/SystemGesturesPointerEventListener.java
@@ -43,6 +43,7 @@
private static final int SWIPE_FROM_TOP = 1;
private static final int SWIPE_FROM_BOTTOM = 2;
private static final int SWIPE_FROM_RIGHT = 3;
+ private static final int SWIPE_FROM_LEFT = 4;
private final Context mContext;
private final int mSwipeStartThreshold;
@@ -127,6 +128,9 @@
} else if (swipe == SWIPE_FROM_RIGHT) {
if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight");
mCallbacks.onSwipeFromRight();
+ } else if (swipe == SWIPE_FROM_LEFT) {
+ if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft");
+ mCallbacks.onSwipeFromLeft();
}
}
break;
@@ -229,6 +233,11 @@
&& elapsed < SWIPE_TIMEOUT_MS) {
return SWIPE_FROM_RIGHT;
}
+ if (fromX <= mSwipeStartThreshold
+ && x > fromX + mSwipeDistanceThreshold
+ && elapsed < SWIPE_TIMEOUT_MS) {
+ return SWIPE_FROM_LEFT;
+ }
return SWIPE_NONE;
}
@@ -265,6 +274,7 @@
void onSwipeFromTop();
void onSwipeFromBottom();
void onSwipeFromRight();
+ void onSwipeFromLeft();
void onFling(int durationMs);
void onDown();
void onUpOrCancel();
diff --git a/services/core/java/com/android/server/policy/WindowOrientationListener.java b/services/core/java/com/android/server/policy/WindowOrientationListener.java
index a32c017..8ef0acb 100644
--- a/services/core/java/com/android/server/policy/WindowOrientationListener.java
+++ b/services/core/java/com/android/server/policy/WindowOrientationListener.java
@@ -49,6 +49,7 @@
"debug.orientation.log", false);
private static final boolean USE_GRAVITY_SENSOR = false;
+ private static final int DEFAULT_BATCH_LATENCY = 100000;
private Handler mHandler;
private SensorManager mSensorManager;
@@ -118,7 +119,12 @@
Slog.d(TAG, "WindowOrientationListener enabled");
}
mOrientationJudge.resetLocked();
- mSensorManager.registerListener(mOrientationJudge, mSensor, mRate, mHandler);
+ if (mSensor.getType() == Sensor.TYPE_ACCELEROMETER) {
+ mSensorManager.registerListener(
+ mOrientationJudge, mSensor, mRate, DEFAULT_BATCH_LATENCY, mHandler);
+ } else {
+ mSensorManager.registerListener(mOrientationJudge, mSensor, mRate, mHandler);
+ }
mEnabled = true;
}
}
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 7108f4a..3ed6ec9 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -23,6 +23,7 @@
import com.android.internal.app.IBatteryStats;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
+import com.android.server.am.RetailDemoModeServiceInternal;
import android.app.ActivityManagerNative;
import android.content.BroadcastReceiver;
@@ -91,6 +92,7 @@
private final ActivityManagerInternal mActivityManagerInternal;
private final InputManagerInternal mInputManagerInternal;
private final InputMethodManagerInternal mInputMethodManagerInternal;
+ private final RetailDemoModeServiceInternal mRetailDemoModeServiceInternal;
private final NotifierHandler mHandler;
private final Intent mScreenOnIntent;
@@ -136,6 +138,7 @@
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class);
+ mRetailDemoModeServiceInternal = LocalServices.getService(RetailDemoModeServiceInternal.class);
mHandler = new NotifierHandler(looper);
mScreenOnIntent = new Intent(Intent.ACTION_SCREEN_ON);
@@ -534,7 +537,9 @@
}
mUserActivityPending = false;
}
-
+ if (mRetailDemoModeServiceInternal != null) {
+ mRetailDemoModeServiceInternal.onUserActivity();
+ }
mPolicy.userActivity();
}
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index 5b9d139..8ce2fd9 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -543,7 +543,8 @@
}
try {
- bluetoothOff = bluetooth == null || !bluetooth.isEnabled();
+ bluetoothOff = bluetooth == null ||
+ bluetooth.getState() == BluetoothAdapter.STATE_OFF;
if (!bluetoothOff) {
Log.w(TAG, "Disabling Bluetooth...");
bluetooth.disable(false); // disable but don't persist new state
@@ -577,7 +578,7 @@
if (!bluetoothOff) {
try {
- bluetoothOff = !bluetooth.isEnabled();
+ bluetoothOff = bluetooth.getState() == BluetoothAdapter.STATE_OFF;
} catch (RemoteException ex) {
Log.e(TAG, "RemoteException during bluetooth shutdown", ex);
bluetoothOff = true;
diff --git a/services/core/java/com/android/server/wm/DimLayerController.java b/services/core/java/com/android/server/wm/DimLayerController.java
index 2b9879e..7f97c46 100644
--- a/services/core/java/com/android/server/wm/DimLayerController.java
+++ b/services/core/java/com/android/server/wm/DimLayerController.java
@@ -1,5 +1,6 @@
package com.android.server.wm;
+import static android.view.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DIM_LAYER;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -250,6 +251,13 @@
duration = getDimLayerFadeDuration(duration);
}
state.dimLayer.show(dimLayer, dimAmount, duration);
+
+ // If we showed a dim layer, make sure to redo the layout because some things depend
+ // on whether a dim layer is showing or not.
+ if (targetAlpha == 0) {
+ mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_LAYOUT;
+ mDisplayContent.layoutNeeded = true;
+ }
}
} else if (state.dimLayer.getLayer() != dimLayer) {
state.dimLayer.setLayer(dimLayer);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f59b2ff..8f8ba1d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -42,7 +42,9 @@
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.storage.IMountService;
+import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Slog;
@@ -55,6 +57,7 @@
import com.android.internal.widget.ILockSettings;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.am.ActivityManagerService;
+import com.android.server.am.RetailDemoModeService;
import com.android.server.audio.AudioService;
import com.android.server.camera.CameraService;
import com.android.server.clipboard.ClipboardService;
@@ -154,6 +157,8 @@
"com.google.android.clockwork.ThermalObserver";
private static final String WEAR_BLUETOOTH_SERVICE_CLASS =
"com.google.android.clockwork.bluetooth.WearBluetoothService";
+ private static final String WEAR_TIME_SERVICE_CLASS =
+ "com.google.android.clockwork.time.WearTimeService";
private static final String ACCOUNT_SERVICE_CLASS =
"com.android.server.accounts.AccountManagerService$Lifecycle";
private static final String CONTENT_SERVICE_CLASS =
@@ -176,7 +181,7 @@
* visual content.
*/
private static final int DEFAULT_SYSTEM_THEME =
- com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar;
+ com.android.internal.R.style.Theme_DeviceDefault_System;
private final int mFactoryTestMode;
private Timer mProfilerSnapshotTimer;
@@ -1160,6 +1165,9 @@
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
mSystemServiceManager.startService(WEAR_BLUETOOTH_SERVICE_CLASS);
+ if (!disableNonCoreServices) {
+ mSystemServiceManager.startService(WEAR_TIME_SERVICE_CLASS);
+ }
}
// Before things start rolling, be sure we have decided whether
@@ -1177,6 +1185,11 @@
// MMS service broker
mmsService = mSystemServiceManager.startService(MmsServiceBroker.class);
+ if (Settings.Global.getInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 0) == 0 ||
+ UserManager.isDeviceInDemoMode(mSystemContext)) {
+ mSystemServiceManager.startService(RetailDemoModeService.class);
+ }
+
// It is now time to start up the app processes...
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "MakeVibratorServiceReady");
diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk
index 0437e1d..50e0662 100644
--- a/services/tests/servicestests/Android.mk
+++ b/services/tests/servicestests/Android.mk
@@ -12,6 +12,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
+ frameworks-base-testutils \
services.core \
services.devicepolicy \
services.net \
@@ -19,7 +20,8 @@
easymocklib \
guava \
android-support-test \
- mockito-target
+ mockito-target \
+ ShortcutManagerTestUtils
LOCAL_JAVA_LIBRARIES := android.test.runner
diff --git a/services/tests/servicestests/res/drawable/icon3.png b/services/tests/servicestests/res/drawable/icon3.png
new file mode 100644
index 0000000..64eb294
--- /dev/null
+++ b/services/tests/servicestests/res/drawable/icon3.png
Binary files differ
diff --git a/services/tests/servicestests/res/values/strings.xml b/services/tests/servicestests/res/values/strings.xml
new file mode 100644
index 0000000..2f9d06c
--- /dev/null
+++ b/services/tests/servicestests/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="shortcut_title1"></string>
+ <string name="shortcut_text1"></string>
+ <string name="shortcut_disabled_message1"></string>
+ <string name="shortcut_title2"></string>
+ <string name="shortcut_text2"></string>
+ <string name="shortcut_disabled_message2"></string>
+</resources>
diff --git a/services/tests/servicestests/res/xml/shortcut_0.xml b/services/tests/servicestests/res/xml/shortcut_0.xml
new file mode 100644
index 0000000..fda001e
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_0.xml
@@ -0,0 +1,2 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_1.xml b/services/tests/servicestests/res/xml/shortcut_1.xml
new file mode 100644
index 0000000..e3f9172
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_1.xml
@@ -0,0 +1,18 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="data1"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_1_alt.xml b/services/tests/servicestests/res/xml/shortcut_1_alt.xml
new file mode 100644
index 0000000..2d5e8e7
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_1_alt.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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1-alt"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="data1"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_1_disable.xml b/services/tests/servicestests/res/xml/shortcut_1_disable.xml
new file mode 100644
index 0000000..e3ee3a0
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_1_disable.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="false"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ />
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_2.xml b/services/tests/servicestests/res/xml/shortcut_2.xml
new file mode 100644
index 0000000..f7ea803
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_2.xml
@@ -0,0 +1,48 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="http://a.b.c/"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms2"
+ android:enabled="true"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ >
+ <intent
+ android:action="action2"
+ android:data="http://a.b.c/2"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml
new file mode 100644
index 0000000..b00ec60
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="action1"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ >
+ <intent
+ android:action="action2"
+ >
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_3.xml b/services/tests/servicestests/res/xml/shortcut_3.xml
new file mode 100644
index 0000000..432ca49
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_3.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="http://a.b.c/"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms2"
+ android:enabled="true"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ >
+ <intent
+ android:action="action2"
+ android:data="http://a.b.c/2"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms3"
+ android:enabled="true"
+ android:icon="@drawable/icon3"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ >
+ <intent android:action="action3" />
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_5.xml b/services/tests/servicestests/res/xml/shortcut_5.xml
new file mode 100644
index 0000000..9551100
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5.xml
@@ -0,0 +1,85 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="http://a.b.c/1"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms2"
+ android:enabled="true"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ >
+ <intent
+ android:action="action2"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms4"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ >
+ <intent
+ android:action="android.intent.action.VIEW2"
+ >
+ </intent>
+ <categories />
+ <categories android:name="" />
+ <categories android:name="cat" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms5"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="action"
+ android:data="http://www/"
+ android:targetPackage="abc"
+ android:targetClass=".xyz"
+ android:mimeType="foo/bar"
+ >
+ <categories android:name="cat1" />
+ <categories android:name="cat2" />
+ <extra android:name="key1" android:value="value1" />
+ <extra android:name="key2" android:value="value2" />
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_5_alt.xml b/services/tests/servicestests/res/xml/shortcut_5_alt.xml
new file mode 100644
index 0000000..f79cd6f
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5_alt.xml
@@ -0,0 +1,74 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1_alt"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="http://a.b.c/1"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms2_alt"
+ android:enabled="true"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ >
+ <intent
+ android:action="action2"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms3_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms4_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms5_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_5_reverse.xml b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml
new file mode 100644
index 0000000..d5b7c8f
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml
@@ -0,0 +1,78 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms5"
+ android:enabled="true"
+ android:icon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ >
+ <intent
+ android:action="action1"
+ android:data="http://a.b.c/1"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ <categories android:name="android.shortcut.media" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms4"
+ android:enabled="true"
+ android:icon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ >
+ <intent
+ android:action="action2"
+ >
+ </intent>
+ <categories android:name="android.shortcut.conversation" />
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="action"
+ >
+ <categories android:name="cat1" />
+ <categories android:name="cat2" />
+ <extra android:name="key1" android:value="value1" />
+ <extra android:name="key2" android:value="value2" />
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_error_1.xml b/services/tests/servicestests/res/xml/shortcut_error_1.xml
new file mode 100644
index 0000000..3990d02
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_1.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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="x1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_error_2.xml b/services/tests/servicestests/res/xml/shortcut_error_2.xml
new file mode 100644
index 0000000..a6f7150
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_2.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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="manifest-shortcut-3"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+ <shortcut
+ android:shortcutId="x2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_error_3.xml b/services/tests/servicestests/res/xml/shortcut_error_3.xml
new file mode 100644
index 0000000..a7b9b84
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_3.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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="manifest-shortcut-3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ />
+ <shortcut
+ android:shortcutId="x3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent
+ android:action="android.intent.action.VIEW"
+ >
+ </intent>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/res/xml/shortcut_error_4.xml b/services/tests/servicestests/res/xml/shortcut_error_4.xml
new file mode 100644
index 0000000..3697bb4
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_4.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <!-- This is valid -->
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent android:action="action1" />
+ </shortcut>
+
+ <!-- Invalid: no intent -->
+ <shortcut
+ android:shortcutId="ms_ignored1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ />
+
+ <!-- Valid: more than one intent; first one will be picked. -->
+ <shortcut
+ android:shortcutId="ms2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent android:action="action2_1" />
+ <intent android:action="action2_2" />
+ </shortcut>
+
+ <!-- Valid: disabled shortcut doesn't need an intent -->
+ <shortcut
+ android:shortcutId="ms3"
+ android:enabled="false"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ />
+
+ <!-- Valid, but disabled shortcut's intent will be ignored. -->
+ <shortcut
+ android:shortcutId="ms4"
+ android:enabled="false"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent android:action="action4" />
+ </shortcut>
+
+ <!-- Invalid, no intent action -->
+ <shortcut
+ android:shortcutId="ms_ignored2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ >
+ <intent android:data="x"/>
+ </shortcut>
+</shortcuts>
diff --git a/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java
new file mode 100644
index 0000000..808f4dd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java
@@ -0,0 +1,130 @@
+/*
+ * 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.internal.util;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.test.mock.MockContentProvider;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/**
+ * Fake for system settings.
+ *
+ * To use, ensure that the Context used by the test code returns a ContentResolver that uses this
+ * provider for the Settings authority:
+ *
+ * class MyTestContext extends MockContext {
+ * ...
+ * private final MockContentResolver mContentResolver;
+ * public MyTestContext(...) {
+ * ...
+ * mContentResolver = new MockContentResolver();
+ * mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ * }
+ * ...
+ * @Override
+ * public ContentResolver getContentResolver() {
+ * return mContentResolver;
+ * }
+ *
+ * As long as the code under test is using the test Context, the actual code under test does not
+ * need to be modified, and can access Settings using the normal static methods:
+ *
+ * Settings.Global.getInt(cr, "my_setting", 0); // Returns 0.
+ * Settings.Global.putInt(cr, "my_setting", 5);
+ * Settings.Global.getInt(cr, "my_setting", 0); // Returns 5.
+ *
+ * Note that this class cannot be used in the same process as real settings. This is because it
+ * works by passing an alternate ContentResolver to Settings operations. Unfortunately, the Settings
+ * class only fetches the content provider from the passed-in ContentResolver the first time it's
+ * used, and after that stores it in a per-process static.
+ *
+ * TODO: evaluate implementing settings change notifications. This would require:
+ *
+ * 1. Making ContentResolver#registerContentObserver non-final and overriding it in
+ * MockContentResolver.
+ * 2. Making FakeSettingsProvider take a ContentResolver argument.
+ * 3. Calling ContentResolver#notifyChange(getUriFor(table, arg), ...) on every settings change.
+ */
+public class FakeSettingsProvider extends MockContentProvider {
+
+ private static final String TAG = FakeSettingsProvider.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final String[] TABLES = { "system", "secure", "global" };
+
+ private final HashMap<String, HashMap<String, String>> mTables = new HashMap<>();
+
+ public FakeSettingsProvider() {
+ for (int i = 0; i < TABLES.length; i++) {
+ mTables.put(TABLES[i], new HashMap<String, String>());
+ }
+ }
+
+ private Uri getUriFor(String table, String key) {
+ switch (table) {
+ case "system":
+ return Settings.System.getUriFor(key);
+ case "secure":
+ return Settings.Secure.getUriFor(key);
+ case "global":
+ return Settings.Global.getUriFor(key);
+ default:
+ throw new UnsupportedOperationException("Unknown settings table " + table);
+ }
+ }
+
+ public Bundle call(String method, String arg, Bundle extras) {
+ // Methods are "GET_system", "GET_global", "PUT_secure", etc.
+ String[] commands = method.split("_", 2);
+ String op = commands[0];
+ String table = commands[1];
+
+ Bundle out = new Bundle();
+ String value;
+ switch (op) {
+ case "GET":
+ value = mTables.get(table).get(arg);
+ if (value != null) {
+ if (DBG) {
+ Log.d(TAG, String.format("Returning fake setting %s.%s = %s",
+ table, arg, value));
+ }
+ out.putString(Settings.NameValueTable.VALUE, value);
+ }
+ break;
+ case "PUT":
+ value = extras.getString(Settings.NameValueTable.VALUE, null);
+ if (DBG) {
+ Log.d(TAG, String.format("Inserting fake setting %s.%s = %s",
+ table, arg, value));
+ }
+ if (value != null) {
+ mTables.get(table).put(arg, value);
+ } else {
+ mTables.get(table).remove(arg);
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown command " + method);
+ }
+
+ return out;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java
new file mode 100644
index 0000000..a18737c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.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 com.android.internal.util;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for FakeSettingsProvider.
+ */
+public class FakeSettingsProviderTest extends AndroidTestCase {
+
+ private MockContentResolver mCr;
+
+ @Override
+ public void setUp() throws Exception {
+ mCr = new MockContentResolver();
+ mCr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ }
+
+ @SmallTest
+ public void testBasicOperation() throws Exception {
+ String settingName = Settings.System.SCREEN_BRIGHTNESS;
+
+ try {
+ Settings.System.getInt(mCr, settingName);
+ fail("FakeSettingsProvider should start off empty.");
+ } catch (Settings.SettingNotFoundException expected) {}
+
+ // Check that fake settings can be written and read back.
+ Settings.System.putInt(mCr, settingName, 123);
+ assertEquals(123, Settings.System.getInt(mCr, settingName));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index 4fae4a7..68a35f1 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -26,6 +26,7 @@
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
@@ -61,12 +62,15 @@
import android.os.MessageQueue.IdleHandler;
import android.os.Process;
import android.os.SystemClock;
+import android.provider.Settings;
import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.util.LogPrinter;
+import com.android.internal.util.FakeSettingsProvider;
import com.android.internal.util.WakeupMessage;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkMonitor;
@@ -118,27 +122,24 @@
}
private class MockContext extends BroadcastInterceptingContext {
+ private final MockContentResolver mContentResolver;
+
MockContext(Context base) {
super(base);
+ mContentResolver = new MockContentResolver();
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
}
@Override
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- // PendingIntents sent by the AlarmManager are not intercepted by
- // BroadcastInterceptingContext so we must really register the receiver.
- // This shouldn't effect the real NetworkMonitors as the action contains a random token.
- if (filter.getAction(0).startsWith("android.net.netmon.lingerExpired")) {
- return getBaseContext().registerReceiver(receiver, filter);
- } else {
- return super.registerReceiver(receiver, filter);
- }
- }
-
- @Override
- public Object getSystemService (String name) {
+ public Object getSystemService(String name) {
if (name == Context.CONNECTIVITY_SERVICE) return mCm;
return super.getSystemService(name);
}
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
}
/**
@@ -641,7 +642,6 @@
public void waitForIdle() {
waitForIdle(TIMEOUT_MS);
}
-
}
private interface Criteria {
@@ -1477,6 +1477,73 @@
defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
}
+ @SmallTest
+ public void testMobileDataAlwaysOn() throws Exception {
+ final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+ final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory");
+ handlerThread.start();
+ NetworkCapabilities filter = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET);
+ final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+ mServiceContext, "testFactory", filter);
+ testFactory.setScoreFilter(40);
+
+ // Register the factory and expect it to start looking for a network.
+ testFactory.expectAddRequests(1);
+ testFactory.register();
+ testFactory.waitForNetworkRequests(1);
+ assertTrue(testFactory.getMyStartRequested());
+
+ // Bring up wifi. The factory stops looking for a network.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ testFactory.expectAddRequests(2); // Because the default request changes score twice.
+ mWiFiNetworkAgent.connect(true);
+ testFactory.waitForNetworkRequests(1);
+ assertFalse(testFactory.getMyStartRequested());
+
+ ContentResolver cr = mServiceContext.getContentResolver();
+
+ // Turn on mobile data always on. The factory starts looking again.
+ testFactory.expectAddRequests(1);
+ Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 1);
+ mService.updateMobileDataAlwaysOn();
+ testFactory.waitForNetworkRequests(2);
+ assertTrue(testFactory.getMyStartRequested());
+
+ // Bring up cell data and check that the factory stops looking.
+ assertEquals(1, mCm.getAllNetworks().length);
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ testFactory.expectAddRequests(2); // Because the cell request changes score twice.
+ mCellNetworkAgent.connect(true);
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
+ testFactory.waitForNetworkRequests(2);
+ assertFalse(testFactory.getMyStartRequested()); // Because the cell network outscores us.
+
+ // Check that cell data stays up.
+ mService.waitForIdle();
+ verifyActiveNetwork(TRANSPORT_WIFI);
+ assertEquals(2, mCm.getAllNetworks().length);
+
+ // Turn off mobile data always on and expect the request to disappear...
+ testFactory.expectRemoveRequests(1);
+ Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 0);
+ mService.updateMobileDataAlwaysOn();
+ testFactory.waitForNetworkRequests(1);
+
+ // ... and cell data to be torn down.
+ cellNetworkCallback.expectCallback(CallbackState.LOST);
+ assertEquals(1, mCm.getAllNetworks().length);
+
+ testFactory.unregister();
+ mCm.unregisterNetworkCallback(cellNetworkCallback);
+ handlerThread.quit();
+ }
+
private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java b/services/tests/servicestests/src/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
new file mode 100644
index 0000000..a30b362
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
@@ -0,0 +1,277 @@
+/*
+ * 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.connectivity.tethering;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import static android.net.ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE;
+import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED;
+import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE;
+
+import android.net.ConnectivityManager;
+import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.os.INetworkManagementService;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetherInterfaceStateMachineTest {
+ private static final String IFACE_NAME = "testnet1";
+ private static final String UPSTREAM_IFACE = "upstream0";
+ private static final String UPSTREAM_IFACE2 = "upstream1";
+
+ @Mock private INetworkManagementService mNMService;
+ @Mock private INetworkStatsService mStatsService;
+ @Mock private IControlsTethering mTetherHelper;
+ @Mock private InterfaceConfiguration mInterfaceConfiguration;
+
+ private final TestLooper mLooper = new TestLooper();
+ private TetherInterfaceStateMachine mTestedSm;
+
+ private void initStateMachine(int interfaceType) throws Exception {
+ mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(), interfaceType,
+ mNMService, mStatsService, mTetherHelper);
+ mTestedSm.start();
+ // Starting the state machine always puts us in a consistent state and notifies
+ // the test of the world that we've changed from an unknown to available state.
+ mLooper.dispatchAll();
+ reset(mNMService, mStatsService, mTetherHelper);
+ when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
+ }
+
+ private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception {
+ initStateMachine(interfaceType);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ if (upstreamIface != null) {
+ dispatchTetherConnectionChanged(upstreamIface);
+ }
+ reset(mNMService, mStatsService, mTetherHelper);
+ when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
+ }
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void startsOutAvailable() {
+ mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(),
+ ConnectivityManager.TETHERING_BLUETOOTH, mNMService, mStatsService, mTetherHelper);
+ mTestedSm.start();
+ mLooper.dispatchAll();
+ verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mTetherHelper, mNMService, mStatsService);
+ }
+
+ @Test
+ public void shouldDoNothingUntilRequested() throws Exception {
+ initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
+ final int [] NOOP_COMMANDS = {
+ TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED,
+ TetherInterfaceStateMachine.CMD_IP_FORWARDING_ENABLE_ERROR,
+ TetherInterfaceStateMachine.CMD_IP_FORWARDING_DISABLE_ERROR,
+ TetherInterfaceStateMachine.CMD_START_TETHERING_ERROR,
+ TetherInterfaceStateMachine.CMD_STOP_TETHERING_ERROR,
+ TetherInterfaceStateMachine.CMD_SET_DNS_FORWARDERS_ERROR,
+ TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED
+ };
+ for (int command : NOOP_COMMANDS) {
+ // None of these commands should trigger us to request action from
+ // the rest of the system.
+ dispatchCommand(command);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+ }
+
+ @Test
+ public void handlesImmediateInterfaceDown() throws Exception {
+ initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
+
+ dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
+ verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void canBeTethered() throws Exception {
+ initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
+
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ InOrder inOrder = inOrder(mTetherHelper, mNMService);
+ inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
+ inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void canUnrequestTethering() throws Exception {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null);
+
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper);
+ inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
+ inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void canBeTetheredAsUsb() throws Exception {
+ initStateMachine(ConnectivityManager.TETHERING_USB);
+
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ InOrder inOrder = inOrder(mTetherHelper, mNMService);
+ inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
+ inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
+ inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
+ inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void handlesFirstUpstreamChange() throws Exception {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null);
+
+ // Telling the state machine about its upstream interface triggers a little more configuration.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder inOrder = inOrder(mNMService);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void handlesChangingUpstream() throws Exception {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNMService, mStatsService);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void canUnrequestTetheringWithUpstream() throws Exception {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
+ inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+
+ @Test
+ public void interfaceDownLeadsToUnavailable() throws Exception {
+ for (boolean shouldThrow : new boolean[]{true, false}) {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null);
+
+ if (shouldThrow) {
+ doThrow(RemoteException.class).when(mNMService).untetherInterface(IFACE_NAME);
+ }
+ dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(
+ IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ }
+ }
+
+ @Test
+ public void usbShouldBeTornDownOnTetherError() throws Exception {
+ initStateMachine(ConnectivityManager.TETHERING_USB);
+
+ doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(
+ IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+ }
+
+ @Test
+ public void shouldTearDownUsbOnUpstreamError() throws Exception {
+ initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null);
+
+ doThrow(RemoteException.class).when(mNMService).enableNat(anyString(), anyString());
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
+ IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR);
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the TetherInterfaceStateMachine.CMD_* constants.
+ */
+ private void dispatchCommand(int command) {
+ mTestedSm.sendMessage(command);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Special override to tell the state machine that the upstream interface has changed.
+ *
+ * @see #dispatchCommand(int)
+ * @param upstreamIface String name of upstream interface (or null)
+ */
+ private void dispatchTetherConnectionChanged(String upstreamIface) {
+ mTestedSm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
+ upstreamIface);
+ mLooper.dispatchAll();
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
new file mode 100644
index 0000000..b8b67e9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -0,0 +1,1593 @@
+/*
+ * 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 static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.cloneShortcutList;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.makeBundle;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
+import android.app.Activity;
+import android.app.IUidObserver;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ILauncherApps;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ShortcutQuery;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.content.pm.ShortcutServiceInternal;
+import android.content.pm.Signature;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.test.InstrumentationTestCase;
+import android.test.mock.MockContext;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.pm.LauncherAppsService.LauncherAppsImpl;
+import com.android.server.pm.ShortcutUser.PackageWithUser;
+
+import org.junit.Assert;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+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.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+
+public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
+ protected 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.
+ */
+ protected static final boolean ENABLE_DUMP = false; // DO NOT SUBMIT WITH true
+
+ protected static final boolean DUMP_IN_TEARDOWN = false; // DO NOT SUBMIT WITH true
+
+ protected static final String[] EMPTY_STRINGS = new String[0]; // Just for readability.
+
+ // public for mockito
+ public class BaseContext extends MockContext {
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case Context.USER_SERVICE:
+ return mMockUserManager;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ return getTestContext().getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mMockPackageManager;
+ }
+
+ @Override
+ public Resources getResources() {
+ return getTestContext().getResources();
+ }
+
+ @Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ // ignore.
+ return null;
+ }
+ }
+
+ /** Context used in the client side */
+ public class ClientContext extends BaseContext {
+ @Override
+ public String getPackageName() {
+ return mInjectedClientPackage;
+ }
+
+ @Override
+ public int getUserId() {
+ return getCallingUserId();
+ }
+ }
+
+ /** Context used in the service side */
+ public class ServiceContext extends BaseContext {
+ long injectClearCallingIdentity() {
+ final int prevCallingUid = mInjectedCallingUid;
+ mInjectedCallingUid = Process.SYSTEM_UID;
+ return prevCallingUid;
+ }
+
+ void injectRestoreCallingIdentity(long token) {
+ mInjectedCallingUid = (int) token;
+ }
+
+ @Override
+ public void startActivityAsUser(@RequiresPermission Intent intent, @Nullable Bundle options,
+ UserHandle userId) {
+ }
+
+ @Override
+ public int getUserId() {
+ return UserHandle.USER_SYSTEM;
+ }
+
+ public PackageInfo injectGetActivitiesWithMetadata(
+ String packageName, @UserIdInt int userId) {
+ return BaseShortcutManagerTest.this.injectGetActivitiesWithMetadata(packageName, userId);
+ }
+
+ public XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
+ return BaseShortcutManagerTest.this.injectXmlMetaData(activityInfo, key);
+ }
+ }
+
+ /** ShortcutService with injection override methods. */
+ protected final class ShortcutServiceTestable extends ShortcutService {
+ final ServiceContext mContext;
+ IUidObserver mUidObserver;
+
+ public ShortcutServiceTestable(ServiceContext context, Looper looper) {
+ super(context, looper);
+ mContext = context;
+ }
+
+ @Override
+ boolean injectShouldPerformVerification() {
+ return true; // Always verify during unit tests.
+ }
+
+ @Override
+ String injectShortcutManagerConstants() {
+ return ConfigConstants.KEY_RESET_INTERVAL_SEC + "=" + (INTERVAL / 1000) + ","
+ + ConfigConstants.KEY_MAX_SHORTCUTS + "=" + MAX_SHORTCUTS + ","
+ + ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "="
+ + MAX_UPDATES_PER_INTERVAL + ","
+ + ConfigConstants.KEY_MAX_ICON_DIMENSION_DP + "=" + MAX_ICON_DIMENSION + ","
+ + ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM + "="
+ + MAX_ICON_DIMENSION_LOWRAM + ","
+ + ConfigConstants.KEY_ICON_FORMAT + "=PNG,"
+ + ConfigConstants.KEY_ICON_QUALITY + "=100";
+ }
+
+ @Override
+ long injectClearCallingIdentity() {
+ return mContext.injectClearCallingIdentity();
+ }
+
+ @Override
+ void injectRestoreCallingIdentity(long token) {
+ mContext.injectRestoreCallingIdentity(token);
+ }
+
+ @Override
+ int injectDipToPixel(int dip) {
+ return dip;
+ }
+
+ @Override
+ long injectCurrentTimeMillis() {
+ return mInjectedCurrentTimeMillis;
+ }
+
+ @Override
+ long injectElapsedRealtime() {
+ // TODO This should be kept separately from mInjectedCurrentTimeMillis, since
+ // this should increase even if we rewind mInjectedCurrentTimeMillis in some tests.
+ return mInjectedCurrentTimeMillis - START_TIME;
+ }
+
+ @Override
+ int injectBinderCallingUid() {
+ return mInjectedCallingUid;
+ }
+
+ @Override
+ int injectGetPackageUid(String packageName, int userId) {
+ return getInjectedPackageInfo(packageName, userId, false).applicationInfo.uid;
+ }
+
+ @Override
+ File injectSystemDataPath() {
+ return new File(mInjectedFilePathRoot, "system");
+ }
+
+ @Override
+ File injectUserDataPath(@UserIdInt int userId) {
+ return new File(mInjectedFilePathRoot, "user-" + userId);
+ }
+
+ @Override
+ void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
+ // Can't check
+ }
+
+ @Override
+ boolean injectIsLowRamDevice() {
+ return mInjectedIsLowRamDevice;
+ }
+
+ @Override
+ void injectRegisterUidObserver(IUidObserver observer, int which) {
+ mUidObserver = observer;
+ }
+
+ @Override
+ PackageManagerInternal injectPackageManagerInternal() {
+ return mMockPackageManagerInternal;
+ }
+
+ @Override
+ boolean hasShortcutHostPermission(@NonNull String callingPackage, int userId) {
+ return mDefaultLauncherChecker.test(callingPackage, userId);
+ }
+
+ @Override
+ PackageInfo injectPackageInfo(String packageName, @UserIdInt int userId,
+ boolean getSignatures) {
+ return getInjectedPackageInfo(packageName, userId, getSignatures);
+ }
+
+ @Override
+ ApplicationInfo injectApplicationInfo(String packageName, @UserIdInt int userId) {
+ PackageInfo pi = injectPackageInfo(packageName, userId, /* getSignatures= */ false);
+ return pi != null ? pi.applicationInfo : null;
+ }
+
+ @Override
+ List<PackageInfo> injectInstalledPackages(@UserIdInt int userId) {
+ return getInstalledPackages(userId);
+ }
+
+ @Override
+ PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
+ return mContext.injectGetActivitiesWithMetadata(packageName, userId);
+ }
+
+ @Override
+ XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
+ return mContext.injectXmlMetaData(activityInfo, key);
+ }
+
+ @Override
+ void injectPostToHandler(Runnable r) {
+ final long token = mContext.injectClearCallingIdentity();
+ r.run();
+ mContext.injectRestoreCallingIdentity(token);
+ }
+
+ @Override
+ void injectEnforceCallingPermission(String permission, String message) {
+ if (!mCallerPermissions.contains(permission)) {
+ throw new SecurityException("Missing permission: " + permission);
+ }
+ }
+
+ @Override
+ void wtf(String message, Exception e) {
+ // During tests, WTF is fatal.
+ fail(message + " exception: " + e);
+ }
+ }
+
+ /** ShortcutManager with injection override methods. */
+ protected class ShortcutManagerTestable extends ShortcutManager {
+ public ShortcutManagerTestable(Context context, ShortcutServiceTestable service) {
+ super(context, service);
+ }
+
+ @Override
+ protected int injectMyUserId() {
+ return UserHandle.getUserId(mInjectedCallingUid);
+ }
+
+ @Override
+ public boolean setDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
+ // Note to simulate the binder RPC, we need to clone the incoming arguments.
+ // Otherwise bad things will happen because they're mutable.
+ return super.setDynamicShortcuts(cloneShortcutList(shortcutInfoList));
+ }
+
+ @Override
+ public boolean addDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
+ // Note to simulate the binder RPC, we need to clone the incoming arguments.
+ return super.addDynamicShortcuts(cloneShortcutList(shortcutInfoList));
+ }
+
+ @Override
+ public boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) {
+ // Note to simulate the binder RPC, we need to clone the incoming arguments.
+ return super.updateShortcuts(cloneShortcutList(shortcutInfoList));
+ }
+ }
+
+ protected class LauncherAppImplTestable extends LauncherAppsImpl {
+ final ServiceContext mContext;
+
+ public LauncherAppImplTestable(ServiceContext context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public void verifyCallingPackage(String callingPackage) {
+ // SKIP
+ }
+
+ @Override
+ void postToPackageMonitorHandler(Runnable r) {
+ final long token = mContext.injectClearCallingIdentity();
+ r.run();
+ mContext.injectRestoreCallingIdentity(token);
+ }
+
+ @Override
+ int injectBinderCallingUid() {
+ return mInjectedCallingUid;
+ }
+
+ @Override
+ long injectClearCallingIdentity() {
+ final int prevCallingUid = mInjectedCallingUid;
+ mInjectedCallingUid = Process.SYSTEM_UID;
+ return prevCallingUid;
+ }
+
+ @Override
+ void injectRestoreCallingIdentity(long token) {
+ mInjectedCallingUid = (int) token;
+ }
+ }
+
+ protected class LauncherAppsTestable extends LauncherApps {
+ public LauncherAppsTestable(Context context, ILauncherApps service) {
+ super(context, service);
+ }
+ }
+
+ public static class ShortcutActivity extends Activity {
+ }
+
+ public static class ShortcutActivity2 extends Activity {
+ }
+
+ public static class ShortcutActivity3 extends Activity {
+ }
+
+ protected ServiceContext mServiceContext;
+ protected ClientContext mClientContext;
+
+ protected ShortcutServiceTestable mService;
+ protected ShortcutManagerTestable mManager;
+ protected ShortcutServiceInternal mInternal;
+
+ protected LauncherAppImplTestable mLauncherAppImpl;
+
+ // LauncherApps has per-instace state, so we need a differnt instance for each launcher.
+ protected final Map<Pair<Integer, String>, LauncherAppsTestable>
+ mLauncherAppsMap = new HashMap<>();
+ protected LauncherAppsTestable mLauncherApps; // Current one
+
+ protected File mInjectedFilePathRoot;
+
+ protected long mInjectedCurrentTimeMillis;
+
+ protected boolean mInjectedIsLowRamDevice;
+
+ protected Locale mInjectedLocale = Locale.ENGLISH;
+
+ protected int mInjectedCallingUid;
+ protected String mInjectedClientPackage;
+
+ protected Map<String, PackageInfo> mInjectedPackages;
+
+ protected Set<PackageWithUser> mUninstalledPackages;
+
+ protected PackageManager mMockPackageManager;
+ protected PackageManagerInternal mMockPackageManagerInternal;
+ protected UserManager mMockUserManager;
+ protected UsageStatsManagerInternal mMockUsageStatsManagerInternal;
+
+ protected static final String CALLING_PACKAGE_1 = "com.android.test.1";
+ protected static final int CALLING_UID_1 = 10001;
+
+ protected static final String CALLING_PACKAGE_2 = "com.android.test.2";
+ protected static final int CALLING_UID_2 = 10002;
+
+ protected static final String CALLING_PACKAGE_3 = "com.android.test.3";
+ protected static final int CALLING_UID_3 = 10003;
+
+ protected static final String CALLING_PACKAGE_4 = "com.android.test.4";
+ protected static final int CALLING_UID_4 = 10004;
+
+ protected static final String LAUNCHER_1 = "com.android.launcher.1";
+ protected static final int LAUNCHER_UID_1 = 10011;
+
+ protected static final String LAUNCHER_2 = "com.android.launcher.2";
+ protected static final int LAUNCHER_UID_2 = 10012;
+
+ protected static final String LAUNCHER_3 = "com.android.launcher.3";
+ protected static final int LAUNCHER_UID_3 = 10013;
+
+ protected static final String LAUNCHER_4 = "com.android.launcher.4";
+ protected static final int LAUNCHER_UID_4 = 10014;
+
+ protected static final int USER_0 = UserHandle.USER_SYSTEM;
+ protected static final int USER_10 = 10;
+ protected static final int USER_11 = 11;
+ protected static final int USER_P0 = 20; // profile of user 0
+
+ protected static final UserHandle HANDLE_USER_0 = UserHandle.of(USER_0);
+ protected static final UserHandle HANDLE_USER_10 = UserHandle.of(USER_10);
+ protected static final UserHandle HANDLE_USER_11 = UserHandle.of(USER_11);
+ protected static final UserHandle HANDLE_USER_P0 = UserHandle.of(USER_P0);
+
+ protected static final UserInfo USER_INFO_0 = withProfileGroupId(
+ new UserInfo(USER_0, "user0",
+ UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY | UserInfo.FLAG_INITIALIZED), 10);
+
+ protected static final UserInfo USER_INFO_10 =
+ new UserInfo(USER_10, "user10", UserInfo.FLAG_INITIALIZED);
+
+ protected static final UserInfo USER_INFO_11 =
+ new UserInfo(USER_11, "user11", UserInfo.FLAG_INITIALIZED);
+
+ protected static final UserInfo USER_INFO_P0 = withProfileGroupId(
+ new UserInfo(USER_P0, "userP0",
+ UserInfo.FLAG_MANAGED_PROFILE), 10);
+
+ protected BiPredicate<String, Integer> mDefaultLauncherChecker =
+ (callingPackage, userId) ->
+ LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
+ || LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage);
+
+ protected static final long START_TIME = 1440000000101L;
+
+ protected static final long INTERVAL = 10000;
+
+ protected static final int MAX_SHORTCUTS = 10;
+
+ protected static final int MAX_UPDATES_PER_INTERVAL = 3;
+
+ protected static final int MAX_ICON_DIMENSION = 128;
+
+ protected static final int MAX_ICON_DIMENSION_LOWRAM = 32;
+
+ protected static final ShortcutQuery QUERY_ALL = new ShortcutQuery();
+
+ protected final ArrayList<String> mCallerPermissions = new ArrayList<>();
+
+ protected final HashMap<String, LinkedHashMap<ComponentName, Integer>> mActivityMetadataResId
+ = new HashMap<>();
+
+ static {
+ QUERY_ALL.setQueryFlags(
+ ShortcutQuery.FLAG_GET_ALL_KINDS);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mServiceContext = spy(new ServiceContext());
+ mClientContext = new ClientContext();
+
+ mMockPackageManager = mock(PackageManager.class);
+ mMockPackageManagerInternal = mock(PackageManagerInternal.class);
+ mMockUserManager = mock(UserManager.class);
+ mMockUsageStatsManagerInternal = mock(UsageStatsManagerInternal.class);
+
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ LocalServices.addService(PackageManagerInternal.class, mMockPackageManagerInternal);
+ LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
+ LocalServices.addService(UsageStatsManagerInternal.class, mMockUsageStatsManagerInternal);
+
+ // Prepare injection values.
+
+ mInjectedCurrentTimeMillis = START_TIME;
+
+ mInjectedPackages = new HashMap<>();
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 1);
+ addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 2);
+ addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 3);
+ addPackage(CALLING_PACKAGE_4, CALLING_UID_4, 10);
+ addPackage(LAUNCHER_1, LAUNCHER_UID_1, 4);
+ addPackage(LAUNCHER_2, LAUNCHER_UID_2, 5);
+ addPackage(LAUNCHER_3, LAUNCHER_UID_3, 6);
+ addPackage(LAUNCHER_4, LAUNCHER_UID_4, 10);
+
+ // CALLING_PACKAGE_3 / LAUNCHER_3 are not backup target.
+ updatePackageInfo(CALLING_PACKAGE_3,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+ updatePackageInfo(LAUNCHER_3,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+
+ mUninstalledPackages = new HashSet<>();
+
+ mInjectedFilePathRoot = new File(getTestContext().getCacheDir(), "test-files");
+
+ deleteAllSavedFiles();
+
+ // Set up users.
+ doAnswer(inv -> {
+ assertSystem();
+ return USER_INFO_0;
+ }).when(mMockUserManager).getUserInfo(eq(USER_0));
+
+ doAnswer(inv -> {
+ assertSystem();
+ return USER_INFO_10;
+ }).when(mMockUserManager).getUserInfo(eq(USER_10));
+
+ doAnswer(inv -> {
+ assertSystem();
+ return USER_INFO_11;
+ }).when(mMockUserManager).getUserInfo(eq(USER_11));
+
+ doAnswer(inv -> {
+ assertSystem();
+ return USER_INFO_P0;
+ }).when(mMockUserManager).getUserInfo(eq(USER_P0));
+
+ // User 0 is always running.
+ when(mMockUserManager.isUserRunning(eq(USER_0))).thenAnswer(new AnswerIsUserRunning(true));
+
+ setUpAppResources();
+
+ // Start the service.
+ initService();
+ setCaller(CALLING_PACKAGE_1);
+
+ // In order to complicate the situation, we set mLocaleChangeSequenceNumber to 1 by
+ // calling this. Running test with mLocaleChangeSequenceNumber == 0 might make us miss
+ // some edge cases.
+ mInternal.onSystemLocaleChangedNoLock();
+ }
+
+ /**
+ * Returns a boolean but also checks if the current UID is SYSTEM_UID.
+ */
+ protected class AnswerIsUserRunning implements Answer<Boolean> {
+ protected final boolean mAnswer;
+
+ protected AnswerIsUserRunning(boolean answer) {
+ mAnswer = answer;
+ }
+
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ assertEquals("isUserRunning() must be called on SYSTEM UID.",
+ Process.SYSTEM_UID, mInjectedCallingUid);
+ return mAnswer;
+ }
+ }
+
+ protected void setUpAppResources() throws Exception {
+ setUpAppResources(/* offset = */ 0);
+ }
+
+ protected void setUpAppResources(int ressIdOffset) throws Exception {
+ // ressIdOffset is used to adjust resource IDs to emulate the case where an updated app
+ // has resource IDs changed.
+
+ doAnswer(pmInvocation -> {
+ assertEquals(Process.SYSTEM_UID, mInjectedCallingUid);
+
+ final String packageName = (String) pmInvocation.getArguments()[0];
+ final int userId = (Integer) pmInvocation.getArguments()[1];
+
+ final Resources res = mock(Resources.class);
+
+ doAnswer(resInvocation -> {
+ final int argResId = (Integer) resInvocation.getArguments()[0];
+
+ return "string-" + packageName + "-user:" + userId + "-res:" + argResId
+ + "/" + mInjectedLocale;
+ }).when(res).getString(anyInt());
+
+ doAnswer(resInvocation -> {
+ final int resId = (Integer) resInvocation.getArguments()[0];
+
+ // Always use the "string" resource type. The type doesn't matter during the test.
+ return packageName + ":string/r" + resId;
+ }).when(res).getResourceName(anyInt());
+
+ doAnswer(resInvocation -> {
+ final String argResName = (String) resInvocation.getArguments()[0];
+ final String argType = (String) resInvocation.getArguments()[1];
+ final String argPackageName = (String) resInvocation.getArguments()[2];
+
+ // See the above code. getResourceName() will just use "r" + res ID as the entry
+ // name.
+ String entryName = argResName;
+ if (entryName.contains("/")) {
+ entryName = ShortcutInfo.getResourceEntryName(entryName);
+ }
+ return Integer.parseInt(entryName.substring(1)) + ressIdOffset;
+ }).when(res).getIdentifier(anyString(), anyString(), anyString());
+ return res;
+ }).when(mMockPackageManager).getResourcesForApplicationAsUser(anyString(), anyInt());
+ }
+
+ protected static UserInfo withProfileGroupId(UserInfo in, int groupId) {
+ in.profileGroupId = groupId;
+ return in;
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (DUMP_IN_TEARDOWN) dumpsysOnLogcat("Teardown");
+
+ shutdownServices();
+
+ super.tearDown();
+ }
+
+ protected Context getTestContext() {
+ return getInstrumentation().getContext();
+ }
+
+ protected ShortcutManager getManager() {
+ return mManager;
+ }
+
+ protected void deleteAllSavedFiles() {
+ // Empty the data directory.
+ if (mInjectedFilePathRoot.exists()) {
+ Assert.assertTrue("failed to delete dir",
+ FileUtils.deleteContents(mInjectedFilePathRoot));
+ }
+ mInjectedFilePathRoot.mkdirs();
+ }
+
+ /** (Re-) init the manager and the service. */
+ protected void initService() {
+ shutdownServices();
+
+ LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
+
+ // Instantiate targets.
+ mService = new ShortcutServiceTestable(mServiceContext, Looper.getMainLooper());
+ mManager = new ShortcutManagerTestable(mClientContext, mService);
+
+ mInternal = LocalServices.getService(ShortcutServiceInternal.class);
+
+ mLauncherAppImpl = new LauncherAppImplTestable(mServiceContext);
+ mLauncherApps = null;
+ mLauncherAppsMap.clear();
+
+ // Send boot sequence events.
+ mService.onBootPhase(SystemService.PHASE_LOCK_SETTINGS_READY);
+
+ // Make sure a call to onSystemLocaleChangedNoLock() before PHASE_BOOT_COMPLETED will be
+ // ignored.
+ final long origSequenceNumber = mService.getLocaleChangeSequenceNumber();
+ mInternal.onSystemLocaleChangedNoLock();
+ assertEquals(origSequenceNumber, mService.getLocaleChangeSequenceNumber());
+
+ mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ }
+
+ protected void shutdownServices() {
+ if (mService != null) {
+ // Flush all the unsaved data from the previous instance.
+ mService.saveDirtyInfo();
+
+ // Make sure everything is consistent.
+ mService.verifyStates();
+ }
+ LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
+
+ mService = null;
+ mManager = null;
+ mInternal = null;
+ mLauncherAppImpl = null;
+ mLauncherApps = null;
+ mLauncherAppsMap.clear();
+ }
+
+ protected void addPackage(String packageName, int uid, int version) {
+ addPackage(packageName, uid, version, packageName);
+ }
+
+ protected Signature[] genSignatures(String... signatures) {
+ final Signature[] sigs = new Signature[signatures.length];
+ for (int i = 0; i < signatures.length; i++){
+ sigs[i] = new Signature(signatures[i].getBytes());
+ }
+ return sigs;
+ }
+
+ protected PackageInfo genPackage(String packageName, int uid, int version, String... signatures) {
+ final PackageInfo pi = new PackageInfo();
+ pi.packageName = packageName;
+ pi.applicationInfo = new ApplicationInfo();
+ pi.applicationInfo.uid = uid;
+ pi.applicationInfo.flags = ApplicationInfo.FLAG_INSTALLED
+ | ApplicationInfo.FLAG_ALLOW_BACKUP;
+ pi.versionCode = version;
+ pi.applicationInfo.versionCode = version;
+ pi.signatures = genSignatures(signatures);
+
+ return pi;
+ }
+
+ protected void addPackage(String packageName, int uid, int version, String... signatures) {
+ mInjectedPackages.put(packageName, genPackage(packageName, uid, version, signatures));
+ }
+
+ protected void updatePackageInfo(String packageName, Consumer<PackageInfo> c) {
+ c.accept(mInjectedPackages.get(packageName));
+ }
+
+ protected void updatePackageVersion(String packageName, int increment) {
+ updatePackageInfo(packageName, pi -> {
+ pi.versionCode += increment;
+ pi.applicationInfo.versionCode += increment;
+ });
+ }
+
+ protected void updatePackageLastUpdateTime(String packageName, long increment) {
+ updatePackageInfo(packageName, pi -> {
+ pi.lastUpdateTime += increment;
+ });
+ }
+
+ protected void uninstallPackage(int userId, String packageName) {
+ if (ENABLE_DUMP) {
+ Log.v(TAG, "Unnstall package " + packageName + " / " + userId);
+ }
+ mUninstalledPackages.add(PackageWithUser.of(userId, packageName));
+ }
+
+ protected void installPackage(int userId, String packageName) {
+ if (ENABLE_DUMP) {
+ Log.v(TAG, "Install package " + packageName + " / " + userId);
+ }
+ mUninstalledPackages.remove(PackageWithUser.of(userId, packageName));
+ }
+
+ PackageInfo getInjectedPackageInfo(String packageName, @UserIdInt int userId,
+ boolean getSignatures) {
+ final PackageInfo pi = mInjectedPackages.get(packageName);
+ if (pi == null) return null;
+
+ final PackageInfo ret = new PackageInfo();
+ ret.packageName = pi.packageName;
+ ret.versionCode = pi.versionCode;
+ ret.lastUpdateTime = pi.lastUpdateTime;
+
+ ret.applicationInfo = new ApplicationInfo(pi.applicationInfo);
+ ret.applicationInfo.uid = UserHandle.getUid(userId, pi.applicationInfo.uid);
+ ret.applicationInfo.packageName = pi.packageName;
+
+ if (mUninstalledPackages.contains(PackageWithUser.of(userId, packageName))) {
+ ret.applicationInfo.flags &= ~ApplicationInfo.FLAG_INSTALLED;
+ }
+
+ if (getSignatures) {
+ ret.signatures = pi.signatures;
+ }
+
+ return ret;
+ }
+
+ protected void addApplicationInfo(PackageInfo pi, List<ApplicationInfo> list) {
+ if (pi != null && pi.applicationInfo != null) {
+ list.add(pi.applicationInfo);
+ }
+ }
+
+ protected List<ApplicationInfo> getInstalledApplications(int userId) {
+ final ArrayList<ApplicationInfo> ret = new ArrayList<>();
+
+ addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_1, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_2, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_3, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_4, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(LAUNCHER_1, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(LAUNCHER_2, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(LAUNCHER_3, userId, false), ret);
+ addApplicationInfo(getInjectedPackageInfo(LAUNCHER_4, userId, false), ret);
+
+ return ret;
+ }
+
+ private void addPackageInfo(PackageInfo pi, List<PackageInfo> list) {
+ if (pi != null) {
+ list.add(pi);
+ }
+ }
+
+ private List<PackageInfo> getInstalledPackages(int userId) {
+ final ArrayList<PackageInfo> ret = new ArrayList<>();
+
+ addPackageInfo(getInjectedPackageInfo(CALLING_PACKAGE_1, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(CALLING_PACKAGE_2, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(CALLING_PACKAGE_3, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(CALLING_PACKAGE_4, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(LAUNCHER_1, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(LAUNCHER_2, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(LAUNCHER_3, userId, false), ret);
+ addPackageInfo(getInjectedPackageInfo(LAUNCHER_4, userId, false), ret);
+
+ return ret;
+ }
+
+ protected void addManifestShortcutResource(ComponentName activity, int resId) {
+ final String packageName = activity.getPackageName();
+ LinkedHashMap<ComponentName, Integer> map = mActivityMetadataResId.get(packageName);
+ if (map == null) {
+ map = new LinkedHashMap<>();
+ mActivityMetadataResId.put(packageName, map);
+ }
+ map.put(activity, resId);
+ }
+
+ protected PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
+ final PackageInfo ret = getInjectedPackageInfo(packageName, userId,
+ /* getSignatures=*/ false);
+
+ final HashMap<ComponentName, Integer> activities = mActivityMetadataResId.get(packageName);
+ if (activities != null) {
+ final ArrayList<ActivityInfo> list = new ArrayList<>();
+
+ for (ComponentName cn : activities.keySet()) {
+ ActivityInfo ai = new ActivityInfo();
+ ai.packageName = cn.getPackageName();
+ ai.name = cn.getClassName();
+ ai.metaData = new Bundle();
+ ai.metaData.putInt(ShortcutParser.METADATA_KEY, activities.get(cn));
+ list.add(ai);
+ }
+ ret.activities = list.toArray(new ActivityInfo[list.size()]);
+ }
+ return ret;
+ }
+
+ protected XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
+ if (!ShortcutParser.METADATA_KEY.equals(key) || activityInfo.metaData == null) {
+ return null;
+ }
+ final int resId = activityInfo.metaData.getInt(key);
+ return getTestContext().getResources().getXml(resId);
+ }
+
+ /** Replace the current calling package */
+ protected void setCaller(String packageName, int userId) {
+ mInjectedClientPackage = packageName;
+ mInjectedCallingUid =
+ Preconditions.checkNotNull(getInjectedPackageInfo(packageName, userId, false),
+ "Unknown package").applicationInfo.uid;
+
+ // Set up LauncherApps for this caller.
+ final Pair<Integer, String> key = Pair.create(userId, packageName);
+ if (!mLauncherAppsMap.containsKey(key)) {
+ mLauncherAppsMap.put(key, new LauncherAppsTestable(mClientContext, mLauncherAppImpl));
+ }
+ mLauncherApps = mLauncherAppsMap.get(key);
+ }
+
+ protected void setCaller(String packageName) {
+ setCaller(packageName, UserHandle.USER_SYSTEM);
+ }
+
+ protected String getCallingPackage() {
+ return mInjectedClientPackage;
+ }
+
+ protected void setDefaultLauncherChecker(BiPredicate<String, Integer> p) {
+ mDefaultLauncherChecker = p;
+ }
+
+ protected void runWithCaller(String packageName, int userId, Runnable r) {
+ final String previousPackage = mInjectedClientPackage;
+ final int previousUserId = UserHandle.getUserId(mInjectedCallingUid);
+
+ setCaller(packageName, userId);
+
+ r.run();
+
+ setCaller(previousPackage, previousUserId);
+ }
+
+ protected void runWithSystemUid(Runnable r) {
+ final int origUid = mInjectedCallingUid;
+ mInjectedCallingUid = Process.SYSTEM_UID;
+ r.run();
+ mInjectedCallingUid = origUid;
+ }
+
+ protected void lookupAndFillInResourceNames(ShortcutInfo si) {
+ runWithSystemUid(() -> si.lookupAndFillInResourceNames(
+ mService.injectGetResourcesForApplicationAsUser(si.getPackage(), si.getUserId())));
+ }
+
+ protected int getCallingUserId() {
+ return UserHandle.getUserId(mInjectedCallingUid);
+ }
+
+ protected UserHandle getCallingUser() {
+ return UserHandle.of(getCallingUserId());
+ }
+
+ /** For debugging */
+ protected void dumpsysOnLogcat() {
+ dumpsysOnLogcat("");
+ }
+
+ protected void dumpsysOnLogcat(String message) {
+ dumpsysOnLogcat(message, false);
+ }
+
+ protected void dumpsysOnLogcat(String message, boolean force) {
+ if (force || !ENABLE_DUMP) return;
+
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(out);
+ mService.dumpInner(pw, null);
+ pw.close();
+
+ Log.v(TAG, "Dumping ShortcutService: " + message);
+ for (String line : out.toString().split("\n")) {
+ Log.v(TAG, line);
+ }
+ }
+
+ /**
+ * For debugging, dump arbitrary file on logcat.
+ */
+ protected void dumpFileOnLogcat(String path) {
+ dumpFileOnLogcat(path, "");
+ }
+
+ protected void dumpFileOnLogcat(String path, String message) {
+ if (!ENABLE_DUMP) return;
+
+ Log.v(TAG, "Dumping file: " + path + " " + message);
+ final StringBuilder sb = new StringBuilder();
+ try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ Log.v(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.
+ */
+ protected void dumpBaseStateFile() {
+ mService.saveDirtyInfo();
+ dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath()
+ + "/system/" + ShortcutService.FILENAME_BASE_STATE);
+ }
+
+ /**
+ * For debugging, dump per-user state file on logcat.
+ */
+ protected void dumpUserFile(int userId) {
+ dumpUserFile(userId, "");
+ }
+
+ protected void dumpUserFile(int userId, String message) {
+ mService.saveDirtyInfo();
+ dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath()
+ + "/user-" + userId
+ + "/" + ShortcutService.FILENAME_USER_PACKAGES, message);
+ }
+
+ protected void waitOnMainThread() throws Throwable {
+ runTestOnUiThread(() -> {});
+ }
+
+ /**
+ * Make a shortcut with an ID.
+ */
+ protected ShortcutInfo makeShortcut(String id) {
+ return makeShortcut(
+ id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ }
+
+ protected ShortcutInfo makeShortcutWithTitle(String id, String title) {
+ return makeShortcut(
+ id, title, /* activity =*/ null, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ }
+
+ /**
+ * Make a shortcut with an ID and timestamp.
+ */
+ protected ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) {
+ final ShortcutInfo s = makeShortcut(
+ id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ s.setTimestamp(timestamp);
+ return s;
+ }
+
+ /**
+ * Make a shortcut with an ID, a timestamp and an activity component
+ */
+ protected ShortcutInfo makeShortcutWithTimestampWithActivity(String id, long timestamp,
+ ComponentName activity) {
+ final ShortcutInfo s = makeShortcut(
+ id, "Title-" + id, activity, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ s.setTimestamp(timestamp);
+ return s;
+ }
+
+ /**
+ * Make a shortcut with an ID and icon.
+ */
+ protected ShortcutInfo makeShortcutWithIcon(String id, Icon icon) {
+ return makeShortcut(
+ id, "Title-" + id, /* activity =*/ null, icon,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ }
+
+ protected ShortcutInfo makePackageShortcut(String packageName, String id) {
+ String origCaller = getCallingPackage();
+
+ setCaller(packageName);
+ ShortcutInfo s = makeShortcut(
+ id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ setCaller(origCaller); // restore the caller
+
+ return s;
+ }
+
+ /**
+ * Make multiple shortcuts with IDs.
+ */
+ protected List<ShortcutInfo> makeShortcuts(String... ids) {
+ final ArrayList<ShortcutInfo> ret = new ArrayList();
+ for (String id : ids) {
+ ret.add(makeShortcut(id));
+ }
+ return ret;
+ }
+
+ protected ShortcutInfo.Builder makeShortcutBuilder() {
+ return new ShortcutInfo.Builder(mClientContext);
+ }
+
+ protected ShortcutInfo makeShortcutWithActivity(String id, ComponentName activity) {
+ return makeShortcut(
+ id, "Title-" + id, activity, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ }
+
+ protected ShortcutInfo makeShortcutWithActivityAndTitle(String id, ComponentName activity,
+ String title) {
+ return makeShortcut(
+ id, title, activity, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
+ }
+
+ protected ShortcutInfo makeShortcutWithActivityAndRank(String id, ComponentName activity,
+ int rank) {
+ return makeShortcut(
+ id, "Title-" + id, activity, /* icon =*/ null,
+ makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), rank);
+ }
+
+ /**
+ * Make a shortcut with details.
+ */
+ protected ShortcutInfo makeShortcut(String id, String title, ComponentName activity,
+ Icon icon, Intent intent, int rank) {
+ final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext)
+ .setId(id)
+ .setActivity(new ComponentName(mClientContext.getPackageName(), "dummy"))
+ .setTitle(title)
+ .setRank(rank)
+ .setIntent(intent);
+ if (icon != null) {
+ b.setIcon(icon);
+ }
+ if (activity != null) {
+ b.setActivity(activity);
+ }
+ final ShortcutInfo s = b.build();
+
+ s.setTimestamp(mInjectedCurrentTimeMillis); // HACK
+
+ return s;
+ }
+
+ /**
+ * Make an intent.
+ */
+ protected 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
+ protected ComponentName makeComponent(Class<?> clazz) {
+ return new ComponentName(mClientContext, clazz);
+ }
+
+ @NonNull
+ protected 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;
+ }
+
+ protected void assertSystem() {
+ assertEquals("Caller must be system", Process.SYSTEM_UID, mInjectedCallingUid);
+ }
+
+ protected void assertResetTimes(long expectedLastResetTime, long expectedNextResetTime) {
+ assertEquals(expectedLastResetTime, mService.getLastResetTimeLocked());
+ assertEquals(expectedNextResetTime, mService.getNextResetTimeLocked());
+ }
+
+ public static List<ShortcutInfo> assertAllNotHaveIcon(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertNull("ID " + s.getId(), s.getIcon());
+ }
+ return actualShortcuts;
+ }
+
+ @NonNull
+ protected List<ShortcutInfo> assertAllHaveFlags(@NonNull List<ShortcutInfo> actualShortcuts,
+ int shortcutFlags) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId() + " doesn't have flags " + shortcutFlags,
+ s.hasFlags(shortcutFlags));
+ }
+ return actualShortcuts;
+ }
+
+ protected ShortcutInfo getPackageShortcut(String packageName, String shortcutId, int userId) {
+ return mService.getPackageShortcutForTest(packageName, shortcutId, userId);
+ }
+
+ protected void assertShortcutExists(String packageName, String shortcutId, int userId) {
+ assertTrue(getPackageShortcut(packageName, shortcutId, userId) != null);
+ }
+
+ protected void assertShortcutNotExists(String packageName, String shortcutId, int userId) {
+ assertTrue(getPackageShortcut(packageName, shortcutId, userId) == null);
+ }
+
+ protected Intent launchShortcutAndGetIntent(
+ @NonNull String packageName, @NonNull String shortcutId, int userId) {
+ reset(mServiceContext);
+ assertTrue(mLauncherApps.startShortcut(packageName, shortcutId, null, null,
+ UserHandle.of(userId)));
+
+ final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mServiceContext).startActivityAsUser(
+ intentCaptor.capture(),
+ any(Bundle.class),
+ eq(UserHandle.of(userId)));
+ return intentCaptor.getValue();
+ }
+
+ protected Intent launchShortcutAndGetIntent_withShortcutInfo(
+ @NonNull String packageName, @NonNull String shortcutId, int userId) {
+ reset(mServiceContext);
+
+ assertTrue(mLauncherApps.startShortcut(
+ getShortcutInfoAsLauncher(packageName, shortcutId, userId), null, null));
+
+ final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mServiceContext).startActivityAsUser(
+ intentCaptor.capture(),
+ any(Bundle.class),
+ eq(UserHandle.of(userId)));
+ return intentCaptor.getValue();
+ }
+
+ protected void assertShortcutLaunchable(@NonNull String packageName, @NonNull String shortcutId,
+ int userId) {
+ assertNotNull(launchShortcutAndGetIntent(packageName, shortcutId, userId));
+ assertNotNull(launchShortcutAndGetIntent_withShortcutInfo(packageName, shortcutId, userId));
+ }
+
+ protected void assertShortcutNotLaunchable(@NonNull String packageName,
+ @NonNull String shortcutId, int userId) {
+ try {
+ final boolean ok = mLauncherApps.startShortcut(packageName, shortcutId, null, null,
+ UserHandle.of(userId));
+ if (!ok) {
+ return; // didn't launch, okay.
+ }
+ fail();
+ } catch (SecurityException expected) {
+ // security exception is okay too.
+ }
+ }
+
+ protected void assertBitmapDirectories(int userId, String... expectedDirectories) {
+ final Set<String> expected = hashSet(set(expectedDirectories));
+
+ final Set<String> actual = new HashSet<>();
+
+ final File[] files = mService.getUserBitmapFilePath(userId).listFiles();
+ if (files != null) {
+ for (File child : files) {
+ if (child.isDirectory()) {
+ actual.add(child.getName());
+ }
+ }
+ }
+
+ assertEquals(expected, actual);
+ }
+
+ protected void assertBitmapFiles(int userId, String packageName, String... expectedFiles) {
+ final Set<String> expected = hashSet(set(expectedFiles));
+
+ final Set<String> actual = new HashSet<>();
+
+ final File[] files = new File(mService.getUserBitmapFilePath(userId), packageName)
+ .listFiles();
+ if (files != null) {
+ for (File child : files) {
+ if (child.isFile()) {
+ actual.add(child.getName());
+ }
+ }
+ }
+
+ assertEquals(expected, actual);
+ }
+
+ protected String getBitmapFilename(int userId, String packageName, String shortcutId) {
+ final ShortcutInfo si = mService.getPackageShortcutForTest(packageName, shortcutId, userId);
+ if (si == null) {
+ return null;
+ }
+ return new File(si.getBitmapPath()).getName();
+ }
+
+ protected List<ShortcutInfo> getCallerShortcuts() {
+ final ShortcutPackage p = mService.getPackageShortcutForTest(
+ getCallingPackage(), getCallingUserId());
+ return p == null ? null : p.getAllShortcutsForTest();
+ }
+
+ protected ShortcutInfo getCallerShortcut(String shortcutId) {
+ return getPackageShortcut(getCallingPackage(), shortcutId, getCallingUserId());
+ }
+
+ protected List<ShortcutInfo> getLauncherShortcuts(String launcher, int userId, int queryFlags) {
+ final List<ShortcutInfo>[] ret = new List[1];
+ runWithCaller(launcher, userId, () -> {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setQueryFlags(queryFlags);
+ ret[0] = mLauncherApps.getShortcuts(q, UserHandle.of(userId));
+ });
+ return ret[0];
+ }
+
+ protected List<ShortcutInfo> getLauncherPinnedShortcuts(String launcher, int userId) {
+ return getLauncherShortcuts(launcher, userId, ShortcutQuery.FLAG_GET_PINNED);
+ }
+
+ protected ShortcutInfo getShortcutInfoAsLauncher(String packageName, String shortcutId,
+ int userId) {
+ final List<ShortcutInfo> infoList =
+ mLauncherApps.getShortcutInfo(packageName, list(shortcutId),
+ UserHandle.of(userId));
+ assertEquals("No shortcutInfo found (or too many of them)", 1, infoList.size());
+ return infoList.get(0);
+ }
+
+ protected Intent genPackageAddIntent(String pakcageName, int userId) {
+ Intent i = new Intent(Intent.ACTION_PACKAGE_ADDED);
+ i.setData(Uri.parse("package:" + pakcageName));
+ i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ return i;
+ }
+
+ protected Intent genPackageDeleteIntent(String pakcageName, int userId) {
+ Intent i = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+ i.setData(Uri.parse("package:" + pakcageName));
+ i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ return i;
+ }
+
+ protected Intent genPackageUpdateIntent(String pakcageName, int userId) {
+ Intent i = new Intent(Intent.ACTION_PACKAGE_ADDED);
+ i.setData(Uri.parse("package:" + pakcageName));
+ i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ i.putExtra(Intent.EXTRA_REPLACING, true);
+ return i;
+ }
+
+ protected Intent genPackageDataClear(String packageName, int userId) {
+ Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
+ i.setData(Uri.parse("package:" + packageName));
+ i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ return i;
+ }
+
+ protected void assertExistsAndShadow(ShortcutPackageItem spi) {
+ assertNotNull(spi);
+ assertTrue(spi.getPackageInfo().isShadow());
+ }
+
+ protected File makeFile(File baseDirectory, String... paths) {
+ File ret = baseDirectory;
+
+ for (String path : paths) {
+ ret = new File(ret, path);
+ }
+
+ return ret;
+ }
+
+ protected boolean bitmapDirectoryExists(String packageName, int userId) {
+ final File path = new File(mService.getUserBitmapFilePath(userId), packageName);
+ return path.isDirectory();
+ }
+ protected static ShortcutQuery buildQuery(long changedSince,
+ String packageName, ComponentName componentName,
+ /* @ShortcutQuery.QueryFlags */ int flags) {
+ return buildQuery(changedSince, packageName, null, componentName, flags);
+ }
+
+ protected static ShortcutQuery buildQuery(long changedSince,
+ String packageName, List<String> shortcutIds, ComponentName componentName,
+ /* @ShortcutQuery.QueryFlags */ int flags) {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setChangedSince(changedSince);
+ q.setPackage(packageName);
+ q.setShortcutIds(shortcutIds);
+ q.setActivity(componentName);
+ q.setQueryFlags(flags);
+ return q;
+ }
+
+ protected static ShortcutQuery buildAllQuery(String packageName) {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setPackage(packageName);
+ q.setQueryFlags(ShortcutQuery.FLAG_GET_ALL_KINDS);
+ return q;
+ }
+
+ protected static ShortcutQuery buildPinnedQuery(String packageName) {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setPackage(packageName);
+ q.setQueryFlags(ShortcutQuery.FLAG_GET_PINNED);
+ return q;
+ }
+
+ protected static ShortcutQuery buildQueryWithFlags(int queryFlags) {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setQueryFlags(queryFlags);
+ return q;
+ }
+
+ protected void backupAndRestore() {
+ int prevUid = mInjectedCallingUid;
+
+ mInjectedCallingUid = Process.SYSTEM_UID; // Only system can call it.
+
+ dumpsysOnLogcat("Before backup");
+
+ final byte[] payload = mService.getBackupPayload(USER_0);
+ if (ENABLE_DUMP) {
+ final String xml = new String(payload);
+ Log.v(TAG, "Backup payload:");
+ for (String line : xml.split("\n")) {
+ Log.v(TAG, line);
+ }
+ }
+
+ // Before doing anything else, uninstall all packages.
+ for (int userId : list(USER_0, USER_P0)) {
+ for (String pkg : list(CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3,
+ LAUNCHER_1, LAUNCHER_2, LAUNCHER_3)) {
+ uninstallPackage(userId, pkg);
+ }
+ }
+
+ shutdownServices();
+
+ deleteAllSavedFiles();
+
+ initService();
+ mService.applyRestore(payload, USER_0);
+
+ // handleUnlockUser will perform the gone package check, but it shouldn't remove
+ // shadow information.
+ mService.handleUnlockUser(USER_0);
+
+ dumpsysOnLogcat("After restore");
+
+ mInjectedCallingUid = prevUid;
+ }
+
+ protected void prepareCrossProfileDataSet() {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list()));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("x1"), makeShortcut("x2"), makeShortcut("x3"),
+ makeShortcut("x4"), makeShortcut("x5"), makeShortcut("x6"))));
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s1", "s2"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("s1", "s2", "s3"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1", "s4"), HANDLE_USER_P0);
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s2", "s3"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("s2", "s3", "s4"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2", "s5"), HANDLE_USER_P0);
+ });
+ runWithCaller(LAUNCHER_3, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s3", "s4"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("s3", "s4", "s5"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3", "s6"), HANDLE_USER_P0);
+ });
+ runWithCaller(LAUNCHER_4, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list(), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list(), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list(), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_4, list(), HANDLE_USER_0);
+ });
+
+ // Launcher on a managed profile is referring ot user 0!
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3", "s4"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s3", "s4", "s5"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("s3", "s4", "s5", "s6"),
+ HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s4", "s1"), HANDLE_USER_P0);
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("x4", "x5"), HANDLE_USER_10);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("x4", "x5", "x6"), HANDLE_USER_10);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("x4", "x5", "x6", "x1"),
+ HANDLE_USER_10);
+ });
+
+ // Then remove some dynamic shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list()));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("x1"), makeShortcut("x2"), makeShortcut("x3"))));
+ });
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
new file mode 100644
index 0000000..0894323
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -0,0 +1,6185 @@
+/*
+ * 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 static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDisabled;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamic;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamicOrPinned;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllEnabled;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIcon;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconFile;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconResId;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIntents;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveTitle;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllImmutable;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllKeyFieldsOnly;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllManifest;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveIntents;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveTitle;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotKeyFieldsOnly;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotManifest;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllPinned;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllStringsResolved;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllUnique;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertBitmapSize;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertBundleEmpty;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackNotReceived;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackReceived;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCannotUpdateImmutable;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicAndPinned;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicOnly;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicShortcutCountExceeded;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.makeBundle;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.pfdToBitmap;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.resetAll;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.Manifest.permission;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ShortcutQuery;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.frameworks.servicestests.R;
+import com.android.server.pm.ShortcutService.ConfigConstants;
+import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
+import com.android.server.pm.ShortcutUser.PackageWithUser;
+
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * 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.ShortcutManagerTest1 \
+ -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+
+
+ * TODO More tests for pinning + manifest shortcuts
+ * TODO Manifest shortcuts + app upgrade -> launcher callback.
+ * Also locale change should trigger launcehr callbacks too, when they use strign resoucres.
+ * (not implemented yet.)
+ * TODO: Add checks with assertAllNotHaveIcon()
+ * TODO: Detailed test for hasShortcutPermissionInner().
+ * TODO: Add tests for the command line functions too.
+ */
+@SmallTest
+public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
+
+ /**
+ * Test for the first launch path, no settings file available.
+ */
+ public void testFirstInitialize() {
+ assertResetTimes(START_TIME, START_TIME + INTERVAL);
+ }
+
+ /**
+ * Test for {@link ShortcutService#getLastResetTimeLocked()} and
+ * {@link ShortcutService#getNextResetTimeLocked()}.
+ */
+ public void testUpdateAndGetNextResetTimeLocked() {
+ assertResetTimes(START_TIME, START_TIME + INTERVAL);
+
+ // Advance clock.
+ mInjectedCurrentTimeMillis += 100;
+
+ // Shouldn't have changed.
+ assertResetTimes(START_TIME, START_TIME + INTERVAL);
+
+ // Advance clock, almost the reset time.
+ mInjectedCurrentTimeMillis = START_TIME + INTERVAL - 1;
+
+ // Shouldn't have changed.
+ assertResetTimes(START_TIME, START_TIME + INTERVAL);
+
+ // Advance clock.
+ mInjectedCurrentTimeMillis += 1;
+
+ assertResetTimes(START_TIME + INTERVAL, START_TIME + 2 * INTERVAL);
+
+ // Advance further; 4 hours since start.
+ mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50;
+
+ assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL);
+ }
+
+ /**
+ * Test for the restoration from saved file.
+ */
+ public void testInitializeFromSavedFile() {
+
+ mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50;
+ assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL);
+
+ mService.saveBaseStateLocked();
+
+ dumpBaseStateFile();
+
+ mService.saveDirtyInfo();
+
+ // 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.
+ }
+
+ public void testLoadConfig() {
+ mService.updateConfigurationLocked(
+ ConfigConstants.KEY_RESET_INTERVAL_SEC + "=123,"
+ + ConfigConstants.KEY_MAX_SHORTCUTS + "=4,"
+ + ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=5,"
+ + ConfigConstants.KEY_MAX_ICON_DIMENSION_DP + "=100,"
+ + ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM + "=50,"
+ + ConfigConstants.KEY_ICON_FORMAT + "=WEBP,"
+ + ConfigConstants.KEY_ICON_QUALITY + "=75");
+ assertEquals(123000, mService.getResetIntervalForTest());
+ assertEquals(4, mService.getMaxDynamicShortcutsForTest());
+ assertEquals(5, mService.getMaxUpdatesPerIntervalForTest());
+ assertEquals(100, mService.getMaxIconDimensionForTest());
+ assertEquals(CompressFormat.WEBP, mService.getIconPersistFormatForTest());
+ assertEquals(75, mService.getIconPersistQualityForTest());
+
+ mInjectedIsLowRamDevice = true;
+ mService.updateConfigurationLocked(
+ ConfigConstants.KEY_MAX_ICON_DIMENSION_DP + "=100,"
+ + ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM + "=50,"
+ + ConfigConstants.KEY_ICON_FORMAT + "=JPEG");
+ assertEquals(ShortcutService.DEFAULT_RESET_INTERVAL_SEC * 1000,
+ mService.getResetIntervalForTest());
+
+ assertEquals(ShortcutService.DEFAULT_MAX_SHORTCUTS_PER_APP,
+ mService.getMaxDynamicShortcutsForTest());
+
+ assertEquals(ShortcutService.DEFAULT_MAX_UPDATES_PER_INTERVAL,
+ mService.getMaxUpdatesPerIntervalForTest());
+
+ assertEquals(50, mService.getMaxIconDimensionForTest());
+
+ assertEquals(CompressFormat.JPEG, mService.getIconPersistFormatForTest());
+
+ assertEquals(ShortcutService.DEFAULT_ICON_PERSIST_QUALITY,
+ mService.getIconPersistQualityForTest());
+ }
+
+ // === Test for app side APIs ===
+
+ /** Test for {@link android.content.pm.ShortcutManager#getMaxShortcutCountForActivity()} */
+ public void testGetMaxDynamicShortcutCount() {
+ assertEquals(MAX_SHORTCUTS, mManager.getMaxShortcutCountForActivity());
+ }
+
+ /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */
+ public void testGetRemainingCallCount() {
+ assertEquals(MAX_UPDATES_PER_INTERVAL, mManager.getRemainingCallCount());
+ }
+
+ public void testGetIconMaxDimensions() {
+ assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxWidth());
+ assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxHeight());
+ }
+
+ /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */
+ public void testGetRateLimitResetTime() {
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50;
+
+ assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime());
+ }
+
+ public void testSetDynamicShortcuts() {
+ setCaller(CALLING_PACKAGE_1, USER_0);
+
+ final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.icon1);
+ final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().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(list(si1, si2)));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2");
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ // TODO: Check fields
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1");
+ assertEquals(1, mManager.getRemainingCallCount());
+
+ assertTrue(mManager.setDynamicShortcuts(list()));
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getRemainingCallCount());
+
+ dumpsysOnLogcat();
+
+ mInjectedCurrentTimeMillis++; // Need to advance the clock for reset to work.
+ mService.resetThrottlingInner(UserHandle.USER_SYSTEM);
+
+ dumpsysOnLogcat();
+
+ assertTrue(mManager.setDynamicShortcuts(list(si2, si3)));
+ assertEquals(2, mManager.getDynamicShortcuts().size());
+
+ // TODO Check max number
+
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"))));
+ });
+ }
+
+ public void testAddDynamicShortcuts() {
+ setCaller(CALLING_PACKAGE_1, USER_0);
+
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+ final ShortcutInfo si2 = makeShortcut("shortcut2");
+ final ShortcutInfo si3 = makeShortcut("shortcut3");
+
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1");
+
+ assertTrue(mManager.addDynamicShortcuts(list(si2, si3)));
+ assertEquals(1, mManager.getRemainingCallCount());
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2", "shortcut3");
+
+ // This should not crash. It'll still consume the quota.
+ assertTrue(mManager.addDynamicShortcuts(list()));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2", "shortcut3");
+
+ mInjectedCurrentTimeMillis += INTERVAL; // reset
+
+ // Add with the same ID
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("shortcut1"))));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2", "shortcut3");
+
+ // TODO Check max number
+
+ // TODO Check fields.
+
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s1"))));
+ });
+ }
+
+ public void testDeleteDynamicShortcuts() {
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+ final ShortcutInfo si2 = makeShortcut("shortcut2");
+ final ShortcutInfo si3 = makeShortcut("shortcut3");
+ final ShortcutInfo si4 = makeShortcut("shortcut4");
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1, si2, si3, si4)));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2", "shortcut3", "shortcut4");
+
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ mManager.removeDynamicShortcuts(list("shortcut1"));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut2", "shortcut3", "shortcut4");
+
+ mManager.removeDynamicShortcuts(list("shortcut1"));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut2", "shortcut3", "shortcut4");
+
+ mManager.removeDynamicShortcuts(list("shortcutXXX"));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut2", "shortcut3", "shortcut4");
+
+ mManager.removeDynamicShortcuts(list("shortcut2", "shortcut4"));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut3");
+
+ mManager.removeDynamicShortcuts(list("shortcut3"));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()));
+
+ // Still 2 calls left.
+ assertEquals(2, mManager.getRemainingCallCount());
+ }
+
+ public void testDeleteAllDynamicShortcuts() {
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+ final ShortcutInfo si2 = makeShortcut("shortcut2");
+ final ShortcutInfo si3 = makeShortcut("shortcut3");
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1, si2, si3)));
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mManager.getDynamicShortcuts()),
+ "shortcut1", "shortcut2", "shortcut3");
+
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ mManager.removeAllDynamicShortcuts();
+ 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(list(si1, si2, si3)));
+ assertEquals(3, mManager.getDynamicShortcuts().size());
+
+ // Still 1 call left
+ assertEquals(1, mManager.getRemainingCallCount());
+ }
+
+ public void testIcons() throws IOException {
+ final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
+ final Icon res64x64 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64);
+ final Icon res512x512 = Icon.createWithResource(getTestContext(), R.drawable.black_512x512);
+
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+ final Icon bmp64x64 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_64x64));
+ final Icon bmp512x512 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_512x512));
+
+ // Set from package 1
+ setCaller(CALLING_PACKAGE_1);
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("res32x32", res32x32),
+ makeShortcutWithIcon("res64x64", res64x64),
+ makeShortcutWithIcon("bmp32x32", bmp32x32),
+ makeShortcutWithIcon("bmp64x64", bmp64x64),
+ makeShortcutWithIcon("bmp512x512", bmp512x512),
+ makeShortcut("none")
+ )));
+
+ // getDynamicShortcuts() shouldn't return icons, thus assertAllNotHaveIcon().
+ assertShortcutIds(assertAllNotHaveIcon(mManager.getDynamicShortcuts()),
+ "res32x32",
+ "res64x64",
+ "bmp32x32",
+ "bmp64x64",
+ "bmp512x512",
+ "none");
+
+ // Call from another caller with the same ID, just to make sure storage is per-package.
+ setCaller(CALLING_PACKAGE_2);
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("res32x32", res512x512),
+ makeShortcutWithIcon("res64x64", res512x512),
+ makeShortcutWithIcon("none", res512x512)
+ )));
+ assertShortcutIds(assertAllNotHaveIcon(mManager.getDynamicShortcuts()),
+ "res32x32",
+ "res64x64",
+ "none");
+
+ // Different profile. Note the names and the contents don't match.
+ setCaller(CALLING_PACKAGE_1, USER_P0);
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("res32x32", res512x512),
+ makeShortcutWithIcon("bmp32x32", bmp512x512)
+ )));
+ assertShortcutIds(assertAllNotHaveIcon(mManager.getDynamicShortcuts()),
+ "res32x32",
+ "bmp32x32");
+
+ // Re-initialize and load from the files.
+ mService.saveDirtyInfo();
+ initService();
+
+ // Load from launcher.
+ Bitmap bmp;
+
+ setCaller(LAUNCHER_1);
+ // Check hasIconResource()/hasIconFile().
+ assertShortcutIds(assertAllHaveIconResId(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res32x32", USER_0))),
+ "res32x32");
+
+ assertShortcutIds(assertAllHaveIconResId(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res64x64", USER_0))),
+ "res64x64");
+
+ assertShortcutIds(assertAllHaveIconFile(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp32x32", USER_0))),
+ "bmp32x32");
+
+ assertShortcutIds(assertAllHaveIconFile(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp64x64", USER_0))),
+ "bmp64x64");
+
+ assertShortcutIds(assertAllHaveIconFile(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp512x512", USER_0))),
+ "bmp512x512");
+
+ assertShortcutIds(assertAllHaveIconResId(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res32x32", USER_P0))),
+ "res32x32");
+ assertShortcutIds(assertAllHaveIconFile(
+ list(getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp32x32", USER_P0))),
+ "bmp32x32");
+
+ // Check
+ assertEquals(
+ R.drawable.black_32x32,
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res32x32", USER_0)));
+
+ assertEquals(
+ R.drawable.black_64x64,
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res64x64", USER_0)));
+
+ assertEquals(
+ 0, // because it's not a resource
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp32x32", USER_0)));
+ assertEquals(
+ 0, // because it's not a resource
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp64x64", USER_0)));
+ assertEquals(
+ 0, // because it's not a resource
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp512x512", USER_0)));
+
+ bmp = pfdToBitmap(mLauncherApps.getShortcutIconFd(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp32x32", USER_0)));
+ assertBitmapSize(32, 32, bmp);
+
+ bmp = pfdToBitmap(mLauncherApps.getShortcutIconFd(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp64x64", USER_0)));
+ assertBitmapSize(64, 64, bmp);
+
+ bmp = pfdToBitmap(mLauncherApps.getShortcutIconFd(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp512x512", USER_0)));
+ assertBitmapSize(128, 128, bmp);
+
+ assertEquals(
+ R.drawable.black_512x512,
+ mLauncherApps.getShortcutIconResId(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "res32x32", USER_P0)));
+ // Should be 512x512, so shrunk.
+ bmp = pfdToBitmap(mLauncherApps.getShortcutIconFd(
+ getShortcutInfoAsLauncher(CALLING_PACKAGE_1, "bmp32x32", USER_P0)));
+ assertBitmapSize(128, 128, bmp);
+
+ // Also check the overload APIs too.
+ assertEquals(
+ R.drawable.black_32x32,
+ mLauncherApps.getShortcutIconResId(CALLING_PACKAGE_1, "res32x32", HANDLE_USER_0));
+ assertEquals(
+ R.drawable.black_64x64,
+ mLauncherApps.getShortcutIconResId(CALLING_PACKAGE_1, "res64x64", HANDLE_USER_0));
+ assertEquals(
+ R.drawable.black_512x512,
+ mLauncherApps.getShortcutIconResId(CALLING_PACKAGE_1, "res32x32", HANDLE_USER_P0));
+ bmp = pfdToBitmap(
+ mLauncherApps.getShortcutIconFd(CALLING_PACKAGE_1, "bmp32x32", HANDLE_USER_P0));
+ assertBitmapSize(128, 128, bmp);
+ }
+
+ public void testCleanupDanglingBitmaps() throws Exception {
+ assertBitmapDirectories(USER_0, EMPTY_STRINGS);
+ assertBitmapDirectories(USER_10, EMPTY_STRINGS);
+
+ // Make some shortcuts with bitmap icons.
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", bmp32x32),
+ makeShortcutWithIcon("s2", bmp32x32),
+ makeShortcutWithIcon("s3", bmp32x32)
+ ));
+ });
+
+ // Increment the time (which actually we don't have to), which is used for filenames.
+ mInjectedCurrentTimeMillis++;
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("s4", bmp32x32),
+ makeShortcutWithIcon("s5", bmp32x32),
+ makeShortcutWithIcon("s6", bmp32x32)
+ ));
+ });
+
+ // Increment the time, which is used for filenames.
+ mInjectedCurrentTimeMillis++;
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ mManager.setDynamicShortcuts(list(
+ ));
+ });
+
+ // For USER-10, let's try without updating the times.
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("10s1", bmp32x32),
+ makeShortcutWithIcon("10s2", bmp32x32),
+ makeShortcutWithIcon("10s3", bmp32x32)
+ ));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("10s4", bmp32x32),
+ makeShortcutWithIcon("10s5", bmp32x32),
+ makeShortcutWithIcon("10s6", bmp32x32)
+ ));
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_10, () -> {
+ mManager.setDynamicShortcuts(list(
+ ));
+ });
+
+ dumpsysOnLogcat();
+
+ // Check files and directories.
+ // Package 3 has no bitmaps, so we don't create a directory.
+ assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
+ assertBitmapDirectories(USER_10, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
+
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_1,
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s1"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s2"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s3")
+ );
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_2,
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s4"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s5"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s6")
+ );
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_3,
+ EMPTY_STRINGS
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_1,
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s1"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s2"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s3")
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_2,
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s4"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s5"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s6")
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_3,
+ EMPTY_STRINGS
+ );
+
+ // Then create random directories and files.
+ makeFile(mService.getUserBitmapFilePath(USER_0), "a.b.c").mkdir();
+ makeFile(mService.getUserBitmapFilePath(USER_0), "d.e.f").mkdir();
+ makeFile(mService.getUserBitmapFilePath(USER_0), "d.e.f", "123").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_0), "d.e.f", "456").createNewFile();
+
+ makeFile(mService.getUserBitmapFilePath(USER_0), CALLING_PACKAGE_3).mkdir();
+
+ makeFile(mService.getUserBitmapFilePath(USER_0), CALLING_PACKAGE_1, "1").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_0), CALLING_PACKAGE_1, "2").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_0), CALLING_PACKAGE_1, "3").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_0), CALLING_PACKAGE_1, "4").createNewFile();
+
+ makeFile(mService.getUserBitmapFilePath(USER_10), "10a.b.c").mkdir();
+ makeFile(mService.getUserBitmapFilePath(USER_10), "10d.e.f").mkdir();
+ makeFile(mService.getUserBitmapFilePath(USER_10), "10d.e.f", "123").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_10), "10d.e.f", "456").createNewFile();
+
+ makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "1").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "2").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "3").createNewFile();
+ makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "4").createNewFile();
+
+ assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3,
+ "a.b.c", "d.e.f");
+
+ // Save and load. When a user is loaded, we do the cleanup.
+ mService.saveDirtyInfo();
+ initService();
+
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_10);
+ mService.handleUnlockUser(20); // Make sure the logic will still work for nonexistent user.
+
+ // The below check is the same as above, except this time USER_0 use the CALLING_PACKAGE_3
+ // directory.
+
+ assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3);
+ assertBitmapDirectories(USER_10, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
+
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_1,
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s1"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s2"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_1, "s3")
+ );
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_2,
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s4"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s5"),
+ getBitmapFilename(USER_0, CALLING_PACKAGE_2, "s6")
+ );
+ assertBitmapFiles(USER_0, CALLING_PACKAGE_3,
+ EMPTY_STRINGS
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_1,
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s1"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s2"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_1, "10s3")
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_2,
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s4"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s5"),
+ getBitmapFilename(USER_10, CALLING_PACKAGE_2, "10s6")
+ );
+ assertBitmapFiles(USER_10, CALLING_PACKAGE_3,
+ EMPTY_STRINGS
+ );
+ }
+
+ protected void checkShrinkBitmap(int expectedWidth, int expectedHeight, int resId, int maxSize) {
+ assertBitmapSize(expectedWidth, expectedHeight,
+ ShortcutService.shrinkBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), resId),
+ maxSize));
+ }
+
+ public void testShrinkBitmap() {
+ checkShrinkBitmap(32, 32, R.drawable.black_512x512, 32);
+ checkShrinkBitmap(511, 511, R.drawable.black_512x512, 511);
+ checkShrinkBitmap(512, 512, R.drawable.black_512x512, 512);
+
+ checkShrinkBitmap(1024, 4096, R.drawable.black_1024x4096, 4096);
+ checkShrinkBitmap(1024, 4096, R.drawable.black_1024x4096, 4100);
+ checkShrinkBitmap(512, 2048, R.drawable.black_1024x4096, 2048);
+
+ checkShrinkBitmap(4096, 1024, R.drawable.black_4096x1024, 4096);
+ checkShrinkBitmap(4096, 1024, R.drawable.black_4096x1024, 4100);
+ checkShrinkBitmap(2048, 512, R.drawable.black_4096x1024, 2048);
+ }
+
+ protected File openIconFileForWriteAndGetPath(int userId, String packageName)
+ throws IOException {
+ // Shortcut IDs aren't used in the path, so just pass the same ID.
+ final FileOutputStreamWithPath out =
+ mService.openIconFileForWrite(userId, makePackageShortcut(packageName, "id"));
+ out.close();
+ return out.getFile();
+ }
+
+ public void testOpenIconFileForWrite() throws IOException {
+ mInjectedCurrentTimeMillis = 1000;
+
+ final File p10_1_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+ final File p10_1_2 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+
+ final File p10_2_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+ final File p10_2_2 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+
+ final File p11_1_1 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+ final File p11_1_2 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+
+ mInjectedCurrentTimeMillis++;
+
+ final File p10_1_3 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+ final File p10_1_4 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+ final File p10_1_5 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+
+ final File p10_2_3 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+ final File p11_1_3 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+
+ // Make sure their paths are all unique
+ assertAllUnique(list(
+ p10_1_1,
+ p10_1_2,
+ p10_1_3,
+ p10_1_4,
+ p10_1_5,
+
+ p10_2_1,
+ p10_2_2,
+ p10_2_3,
+
+ p11_1_1,
+ p11_1_2,
+ p11_1_3
+ ));
+
+ // Check each set has the same parent.
+ assertEquals(p10_1_1.getParent(), p10_1_2.getParent());
+ assertEquals(p10_1_1.getParent(), p10_1_3.getParent());
+ assertEquals(p10_1_1.getParent(), p10_1_4.getParent());
+ assertEquals(p10_1_1.getParent(), p10_1_5.getParent());
+
+ assertEquals(p10_2_1.getParent(), p10_2_2.getParent());
+ assertEquals(p10_2_1.getParent(), p10_2_3.getParent());
+
+ assertEquals(p11_1_1.getParent(), p11_1_2.getParent());
+ assertEquals(p11_1_1.getParent(), p11_1_3.getParent());
+
+ // Check the parents are still unique.
+ assertAllUnique(list(
+ p10_1_1.getParent(),
+ p10_2_1.getParent(),
+ p11_1_1.getParent()
+ ));
+
+ // All files created at the same time for the same package/user, expcet for the first ones,
+ // will have "_" in the path.
+ assertFalse(p10_1_1.getName().contains("_"));
+ assertTrue(p10_1_2.getName().contains("_"));
+ assertFalse(p10_1_3.getName().contains("_"));
+ assertTrue(p10_1_4.getName().contains("_"));
+ assertTrue(p10_1_5.getName().contains("_"));
+
+ assertFalse(p10_2_1.getName().contains("_"));
+ assertTrue(p10_2_2.getName().contains("_"));
+ assertFalse(p10_2_3.getName().contains("_"));
+
+ assertFalse(p11_1_1.getName().contains("_"));
+ assertTrue(p11_1_2.getName().contains("_"));
+ assertFalse(p11_1_3.getName().contains("_"));
+ }
+
+ public void testUpdateShortcuts() {
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"),
+ makeShortcut("s2"),
+ makeShortcut("s3"),
+ makeShortcut("s4"),
+ makeShortcut("s5")
+ )));
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"),
+ makeShortcut("s2"),
+ makeShortcut("s3"),
+ makeShortcut("s4"),
+ makeShortcut("s5")
+ )));
+ });
+ runWithCaller(LAUNCHER_1, UserHandle.USER_SYSTEM, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2", "s3"),
+ getCallingUser());
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s4", "s5"),
+ getCallingUser());
+ });
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ mManager.removeDynamicShortcuts(list("s1"));
+ mManager.removeDynamicShortcuts(list("s2"));
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ mManager.removeDynamicShortcuts(list("s1"));
+ mManager.removeDynamicShortcuts(list("s3"));
+ mManager.removeDynamicShortcuts(list("s5"));
+ });
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(
+ mManager.getDynamicShortcuts()),
+ "s3", "s4", "s5");
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s2", "s3");
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(
+ mManager.getDynamicShortcuts()),
+ "s2", "s4");
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s4", "s5");
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ ShortcutInfo s2 = makeShortcutBuilder()
+ .setId("s2")
+ .setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
+ .build();
+
+ ShortcutInfo s4 = makeShortcutBuilder()
+ .setId("s4")
+ .setTitle("new title")
+ .build();
+
+ mManager.updateShortcuts(list(s2, s4));
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ ShortcutInfo s2 = makeShortcutBuilder()
+ .setId("s2")
+ .setIntent(makeIntent(Intent.ACTION_ANSWER, ShortcutActivity.class,
+ "key1", "val1"))
+ .build();
+
+ ShortcutInfo s4 = makeShortcutBuilder()
+ .setId("s4")
+ .setIntent(new Intent(Intent.ACTION_ALL_APPS))
+ .build();
+
+ mManager.updateShortcuts(list(s2, s4));
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(
+ mManager.getDynamicShortcuts()),
+ "s3", "s4", "s5");
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s2", "s3");
+
+ ShortcutInfo s = getCallerShortcut("s2");
+ assertTrue(s.hasIconResource());
+ assertEquals(R.drawable.black_32x32, s.getIconResourceId());
+ assertEquals("string/r" + R.drawable.black_32x32, s.getIconResName());
+ assertEquals("Title-s2", s.getTitle());
+
+ s = getCallerShortcut("s4");
+ assertFalse(s.hasIconResource());
+ assertEquals(0, s.getIconResourceId());
+ assertEquals("new title", s.getTitle());
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(
+ mManager.getDynamicShortcuts()),
+ "s2", "s4");
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s4", "s5");
+
+ ShortcutInfo s = getCallerShortcut("s2");
+ assertFalse(s.hasIconResource());
+ assertEquals(0, s.getIconResourceId());
+ assertEquals("Title-s2", s.getTitle());
+ assertEquals(Intent.ACTION_ANSWER, s.getIntent().getAction());
+ assertEquals(1, s.getIntent().getExtras().size());
+
+ s = getCallerShortcut("s4");
+ assertFalse(s.hasIconResource());
+ assertEquals(0, s.getIconResourceId());
+ assertEquals("Title-s4", s.getTitle());
+ assertEquals(Intent.ACTION_ALL_APPS, s.getIntent().getAction());
+ assertBundleEmpty(s.getIntent().getExtras());
+ });
+ // TODO Check with other fields too.
+
+ // TODO Check bitmap removal too.
+
+ runWithCaller(CALLING_PACKAGE_2, USER_11, () -> {
+ mManager.updateShortcuts(list());
+ });
+ }
+
+ // === Test for launcher side APIs ===
+
+ public void testGetShortcuts() {
+
+ // Set up shortcuts.
+
+ setCaller(CALLING_PACKAGE_1);
+ final ShortcutInfo s1_1 = makeShortcut("s1");
+ final ShortcutInfo s1_2 = makeShortcut("s2");
+
+ assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2)));
+
+ // Because setDynamicShortcuts will update the timestamps when ranks are changing,
+ // we explicitly set timestamps here.
+ getCallerShortcut("s1").setTimestamp(5000);
+ getCallerShortcut("s2").setTimestamp(1000);
+
+ setCaller(CALLING_PACKAGE_2);
+ final ShortcutInfo s2_2 = makeShortcut("s2");
+ final ShortcutInfo s2_3 = makeShortcutWithActivity("s3",
+ makeComponent(ShortcutActivity2.class));
+ final ShortcutInfo s2_4 = makeShortcutWithActivity("s4",
+ makeComponent(ShortcutActivity.class));
+ assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4)));
+
+ getCallerShortcut("s2").setTimestamp(1500);
+ getCallerShortcut("s3").setTimestamp(3000);
+ getCallerShortcut("s4").setTimestamp(500);
+
+ setCaller(CALLING_PACKAGE_3);
+ final ShortcutInfo s3_2 = makeShortcut("s3");
+ assertTrue(mManager.setDynamicShortcuts(list(s3_2)));
+
+ getCallerShortcut("s3").setTimestamp(START_TIME + 5000);
+
+ setCaller(LAUNCHER_1);
+
+ // Get dynamic
+ assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertAllStringsResolved(
+ assertShortcutIds(
+ assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2")))));
+
+ // Get pinned
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_PINNED), getCallingUser())
+ /* none */);
+
+ // Get both, with timestamp
+ assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllNotKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2,
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC),
+ getCallingUser())),
+ "s2", "s3"))));
+
+ // FLAG_GET_KEY_FIELDS_ONLY
+ assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2,
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY),
+ getCallingUser())),
+ "s2", "s3"))));
+
+ // Filter by activity
+ assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllNotKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 0, CALLING_PACKAGE_2,
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC),
+ getCallingUser())),
+ "s4"))));
+
+ // With ID.
+ assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2, list("s3"),
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY),
+ getCallingUser())),
+ "s3"))));
+ assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2, list("s3", "s2", "ss"),
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY),
+ getCallingUser())),
+ "s2", "s3"))));
+ assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2, list("s3x", "s2x"),
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY),
+ getCallingUser()))
+ /* empty */))));
+ assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2, list(),
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY),
+ getCallingUser()))
+ /* empty */))));
+
+ // Pin some shortcuts.
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s3", "s4"), getCallingUser());
+
+ // Pinned ones only
+ assertAllPinned(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+ assertAllNotKeyFieldsOnly(mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 1000, CALLING_PACKAGE_2,
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_PINNED),
+ getCallingUser())),
+ "s3"))));
+
+ // All packages.
+ assertShortcutIds(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 5000, /* package= */ null,
+ /* activity =*/ null,
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED),
+ getCallingUser())),
+ "s1", "s3");
+
+ assertExpectException(
+ IllegalArgumentException.class, "package name must also be set", () -> {
+ mLauncherApps.getShortcuts(buildQuery(
+ /* time =*/ 0, /* package= */ null, list("id"),
+ /* activity =*/ null, /* flags */ 0), getCallingUser());
+ });
+
+ // TODO More tests: pinned but dynamic.
+ }
+
+ public void testGetShortcuts_shortcutKinds() throws Exception {
+ // Create 3 manifest and 3 dynamic shortcuts
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_3);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+
+ // Pin 2 and 3
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2", "ms3", "s2", "s3"),
+ HANDLE_USER_0);
+ });
+
+ // Remove ms3 and s3
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+ });
+
+ // Check their status.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "ms3", "s1", "s2", "s3")
+
+ .selectByIds("ms1", "ms2")
+ .areAllManifest()
+ .areAllImmutable()
+ .areAllNotDynamic()
+
+ .revertToOriginalList()
+ .selectByIds("ms3")
+ .areAllNotManifest()
+ .areAllImmutable()
+ .areAllDisabled()
+ .areAllNotDynamic()
+
+ .revertToOriginalList()
+ .selectByIds("s1", "s2")
+ .areAllNotManifest()
+ .areAllMutable()
+ .areAllDynamic()
+
+ .revertToOriginalList()
+ .selectByIds("s3")
+ .areAllNotManifest()
+ .areAllMutable()
+ .areAllEnabled()
+ .areAllNotDynamic()
+
+ .revertToOriginalList()
+ .selectByIds("s1", "ms1")
+ .areAllNotPinned()
+
+ .revertToOriginalList()
+ .selectByIds("s2", "s3", "ms2", "ms3")
+ .areAllPinned()
+ ;
+ });
+
+ // Finally, actual tests.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0))
+ .haveIds("s1", "s2");
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(ShortcutQuery.FLAG_GET_MANIFEST), HANDLE_USER_0))
+ .haveIds("ms1", "ms2");
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0))
+ .haveIds("s2", "s3", "ms2", "ms3");
+
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED
+ ), HANDLE_USER_0))
+ .haveIds("s1", "s2", "s3", "ms2", "ms3");
+
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(
+ ShortcutQuery.FLAG_GET_MANIFEST | ShortcutQuery.FLAG_GET_PINNED
+ ), HANDLE_USER_0))
+ .haveIds("ms1", "s2", "s3", "ms2", "ms3");
+
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(
+ ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_MANIFEST
+ ), HANDLE_USER_0))
+ .haveIds("ms1", "ms2", "s1", "s2");
+
+ assertWith(mLauncherApps.getShortcuts(
+ buildQueryWithFlags(
+ ShortcutQuery.FLAG_GET_ALL_KINDS
+ ), HANDLE_USER_0))
+ .haveIds("ms1", "ms2", "ms3", "s1", "s2", "s3");
+ });
+ }
+
+ public void testGetShortcuts_resolveStrings() throws Exception {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, "dummy"))
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setIntent(makeIntent("action", ShortcutActivity.class))
+ .build();
+ mManager.setDynamicShortcuts(list(si));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, "dummy"))
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setIntent(makeIntent("action", ShortcutActivity.class))
+ .build();
+ mManager.setDynamicShortcuts(list(si));
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ final ShortcutQuery q = new ShortcutQuery();
+ q.setQueryFlags(ShortcutQuery.FLAG_GET_DYNAMIC);
+
+ // USER 0
+ List<ShortcutInfo> ret = assertShortcutIds(
+ assertAllStringsResolved(mLauncherApps.getShortcuts(q, HANDLE_USER_0)),
+ "id");
+ assertEquals("string-com.android.test.1-user:0-res:10/en", ret.get(0).getTitle());
+ assertEquals("string-com.android.test.1-user:0-res:11/en", ret.get(0).getText());
+ assertEquals("string-com.android.test.1-user:0-res:12/en",
+ ret.get(0).getDisabledMessage());
+
+ // USER P0
+ ret = assertShortcutIds(
+ assertAllStringsResolved(mLauncherApps.getShortcuts(q, HANDLE_USER_P0)),
+ "id");
+ assertEquals("string-com.android.test.1-user:20-res:10/en", ret.get(0).getTitle());
+ assertEquals("string-com.android.test.1-user:20-res:11/en", ret.get(0).getText());
+ assertEquals("string-com.android.test.1-user:20-res:12/en",
+ ret.get(0).getDisabledMessage());
+ });
+ }
+
+ // TODO resource
+ 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(list(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(list(s2_1)));
+ dumpsysOnLogcat();
+
+ // Pin some.
+ setCaller(LAUNCHER_1);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2"), getCallingUser());
+
+ dumpsysOnLogcat();
+
+ // Delete some.
+ setCaller(CALLING_PACKAGE_1);
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+ mManager.removeDynamicShortcuts(list("s2"));
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+
+ dumpsysOnLogcat();
+
+ setCaller(LAUNCHER_1);
+ List<ShortcutInfo> list;
+
+ // Note we don't guarantee the orders.
+ list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents(
+ assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcutInfo(CALLING_PACKAGE_1,
+ list("s2", "s1", "s3", null), getCallingUser())))),
+ "s1", "s2");
+ assertEquals("Title 1", findById(list, "s1").getTitle());
+ assertEquals("Title 2", findById(list, "s2").getTitle());
+
+ assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents(
+ mLauncherApps.getShortcutInfo(CALLING_PACKAGE_1,
+ list("s3"), getCallingUser())))
+ /* none */);
+
+ list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents(
+ mLauncherApps.getShortcutInfo(CALLING_PACKAGE_2,
+ list("s1", "s2", "s3"), getCallingUser()))),
+ "s1");
+ assertEquals("ABC", findById(list, "s1").getTitle());
+ }
+
+ public void testPinShortcutAndGetPinnedShortcuts() {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000);
+ final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000);
+
+ assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2)));
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500);
+ final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000);
+ final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500);
+ assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4)));
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s2", 1000);
+ assertTrue(mManager.setDynamicShortcuts(list(s3_2)));
+ });
+
+ // Pin some.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2", "s3"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s3", "s4", "s5"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3,
+ list("s3"), getCallingUser()); // Note ID doesn't exist
+ });
+
+ // Delete some.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+ mManager.removeDynamicShortcuts(list("s2"));
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+
+ assertShortcutIds(mManager.getDynamicShortcuts(), "s1");
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4");
+ mManager.removeDynamicShortcuts(list("s3"));
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4");
+
+ assertShortcutIds(mManager.getDynamicShortcuts(), "s2", "s4");
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts() /* none */);
+ mManager.removeDynamicShortcuts(list("s2"));
+ assertShortcutIds(mManager.getPinnedShortcuts() /* none */);
+
+ assertEmpty(mManager.getDynamicShortcuts());
+ });
+
+ // Get pinned shortcuts from launcher
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists.
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))),
+ "s2");
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))),
+ "s3", "s4");
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_3,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))))
+ /* none */);
+ });
+ }
+
+ /**
+ * This is similar to the above test, except it used "disable" instead of "remove". It also
+ * does "enable".
+ */
+ public void testDisableAndEnableShortcuts() {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000);
+ final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000);
+
+ assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2)));
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500);
+ final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000);
+ final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500);
+ assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4)));
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s2", 1000);
+ assertTrue(mManager.setDynamicShortcuts(list(s3_2)));
+ });
+
+ // Pin some.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2", "s3"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s3", "s4", "s5"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3,
+ list("s3"), getCallingUser()); // Note ID doesn't exist
+ });
+
+ // Disable some.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+
+ mManager.disableShortcuts(list("s2"));
+
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+ assertShortcutIds(mManager.getDynamicShortcuts(), "s1");
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4");
+
+ // disable should work even if a shortcut is not dynamic, so try calling "remove" first
+ // here.
+ mManager.removeDynamicShortcuts(list("s3"));
+ mManager.disableShortcuts(list("s3"));
+
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4");
+ assertShortcutIds(mManager.getDynamicShortcuts(), "s2", "s4");
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts() /* none */);
+
+ mManager.disableShortcuts(list("s2"));
+
+ assertShortcutIds(mManager.getPinnedShortcuts() /* none */);
+
+ assertEmpty(mManager.getDynamicShortcuts());
+ assertEmpty(getCallerShortcuts());
+ });
+
+ // Get pinned shortcuts from launcher
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists.
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllDisabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))),
+ "s2");
+ assertFalse(mLauncherApps.startShortcut(
+ CALLING_PACKAGE_1, "s2", null, null, HANDLE_USER_0));
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3", "s4");
+ assertFalse(mLauncherApps.startShortcut(
+ CALLING_PACKAGE_2, "s3", null, null, HANDLE_USER_0));
+ assertTrue(mLauncherApps.startShortcut(
+ CALLING_PACKAGE_2, "s4", null, null, HANDLE_USER_0));
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_3,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))))
+ /* none */);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.enableShortcuts(list("s2"));
+
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s2");
+ assertShortcutIds(mManager.getDynamicShortcuts(), "s1");
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists.
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))),
+ "s2");
+ assertTrue(mLauncherApps.startShortcut(
+ CALLING_PACKAGE_1, "s2", null, null, HANDLE_USER_0));
+ });
+ }
+
+ public void testPinShortcutAndGetPinnedShortcuts_multi() {
+ // Create some shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+
+ dumpsysOnLogcat();
+
+ // Pin some.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s3", "s4"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s1", "s2", "s4"), getCallingUser());
+ });
+
+ dumpsysOnLogcat();
+
+ // Delete some.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3");
+ mManager.removeDynamicShortcuts(list("s3"));
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s3");
+ });
+
+ dumpsysOnLogcat();
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s1", "s2");
+ mManager.removeDynamicShortcuts(list("s1"));
+ mManager.removeDynamicShortcuts(list("s3"));
+ assertShortcutIds(mManager.getPinnedShortcuts(), "s1", "s2");
+ });
+
+ dumpsysOnLogcat();
+
+ // Get pinned shortcuts from launcher
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3");
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s1", "s2");
+
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2", "s3");
+
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2");
+ });
+
+ dumpsysOnLogcat();
+
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ // Launcher2 still has no pinned ones.
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))
+ /* none */);
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))
+ /* none */);
+
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s2");
+
+ // Now pin some.
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1", "s2"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s1", "s2"), getCallingUser());
+
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2");
+
+ // S1 was not visible to it, so shouldn't be pinned.
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s2");
+ });
+
+ // Re-initialize and load from the files.
+ mService.saveDirtyInfo();
+ initService();
+
+ // Load from file.
+ mService.handleUnlockUser(USER_0);
+
+ // Make sure package info is restored too.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3");
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s1", "s2");
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s1", "s2");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED
+ | ShortcutQuery.FLAG_GET_DYNAMIC), getCallingUser())),
+ "s2");
+ });
+
+ // Delete all dynamic.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeAllDynamicShortcuts();
+
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()), "s1", "s2", "s3");
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ mManager.removeAllDynamicShortcuts();
+
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()), "s2", "s1");
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3");
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s1", "s2");
+
+ // from all packages.
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, null,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s1", "s2", "s3");
+
+ // Update pined. Note s2 and s3 are actually available, but not visible to this
+ // launcher, so still can't be pinned.
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s3", "s4"),
+ getCallingUser());
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3");
+ });
+ // Re-publish s1.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s1"))));
+
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()), "s1");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()), "s1", "s2", "s3");
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s3");
+
+ // Now "s1" is visible, so can be pinned.
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1", "s2", "s3", "s4"),
+ getCallingUser());
+
+ assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))),
+ "s1", "s3");
+ });
+
+ // Now clear pinned shortcuts. First, from launcher 1.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list(), getCallingUser());
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list(), getCallingUser());
+
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()).size());
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()).size());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()), "s1");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()), "s1", "s2");
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()), "s2");
+ });
+
+ // Clear all pins from launcher 2.
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list(), getCallingUser());
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list(), getCallingUser());
+
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()).size());
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()).size());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()), "s1");
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+ }
+
+ public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
+ // Create some shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+
+ // Pin some shortcuts and see the result.
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s1", "s2", "s3"), HANDLE_USER_0);
+ });
+
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s2", "s3"), HANDLE_USER_0);
+ });
+
+ runWithCaller(LAUNCHER_2, USER_P0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s3"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s3"), HANDLE_USER_0);
+ });
+
+ runWithCaller(LAUNCHER_2, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1", "s2", "s3"), HANDLE_USER_10);
+ });
+
+ // Cross profile pinning.
+ final int PIN_AND_DYNAMIC = ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC;
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_10, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_10)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_10)),
+ "s1", "s2", "s3", "s4", "s5", "s6");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_10)),
+ "s1", "s2", "s3", "s4", "s5", "s6");
+ });
+
+ // Remove some dynamic shortcuts.
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"))));
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s3");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_10, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_10)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_10)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_10)),
+ "s1", "s2", "s3");
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+
+ // Save & load and make sure we still have the same information.
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s2", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_P0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s3");
+
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), HANDLE_USER_0)),
+ "s3");
+ assertShortcutIds(assertAllDynamic(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, ShortcutQuery.FLAG_GET_DYNAMIC), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllDynamicOrPinned(
+ mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2,
+ /* activity =*/ null, PIN_AND_DYNAMIC), HANDLE_USER_0)),
+ "s1", "s3");
+
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_1, "s3", USER_0);
+
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s1", USER_0);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_2, "s2", USER_0);
+ assertShortcutLaunchable(CALLING_PACKAGE_2, "s3", USER_0);
+
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s1", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s2", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s3", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s4", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s5", USER_10);
+ assertShortcutNotLaunchable(CALLING_PACKAGE_1, "s6", USER_10);
+ });
+ }
+
+ public void testStartShortcut() {
+ // 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(list(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(list(s2_1)));
+
+ // Pin all.
+ setCaller(LAUNCHER_1);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1", "s2"), getCallingUser());
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s1"), getCallingUser());
+
+ // Just to make it complicated, delete some.
+ setCaller(CALLING_PACKAGE_1);
+ mManager.removeDynamicShortcuts(list("s2"));
+
+ // intent and check.
+ setCaller(LAUNCHER_1);
+
+ Intent intent;
+ intent = launchShortcutAndGetIntent(CALLING_PACKAGE_1, "s1", USER_0);
+ assertEquals(ShortcutActivity2.class.getName(), intent.getComponent().getClassName());
+
+
+ intent = launchShortcutAndGetIntent(CALLING_PACKAGE_1, "s2", USER_0);
+ assertEquals(ShortcutActivity3.class.getName(), intent.getComponent().getClassName());
+
+ intent = launchShortcutAndGetIntent(CALLING_PACKAGE_2, "s1", USER_0);
+ assertEquals(ShortcutActivity.class.getName(), intent.getComponent().getClassName());
+
+ // TODO Check extra, etc
+ }
+
+ public void testLauncherCallback() throws Throwable {
+ // Disable throttling for this test.
+ mService.updateConfigurationLocked(
+ ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999,"
+ + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
+ );
+
+ LauncherApps.Callback c0 = mock(LauncherApps.Callback.class);
+
+ // Set listeners
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper()));
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+
+ waitOnMainThread();
+ ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("s1", "s2", "s3")
+ .areAllWithKeyFieldsOnly()
+ .areAllDynamic();
+
+ // From different package.
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_2),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("s1", "s2", "s3")
+ .areAllWithKeyFieldsOnly()
+ .areAllDynamic();
+
+ // Different user, callback shouldn't be called.
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ waitOnMainThread();
+ verify(c0, times(0)).onShortcutsChanged(
+ anyString(),
+ any(List.class),
+ any(UserHandle.class)
+ );
+
+ // Test for addDynamicShortcuts.
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ dumpsysOnLogcat("before addDynamicShortcuts");
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
+ });
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("s1", "s2", "s3", "s4")
+ .areAllWithKeyFieldsOnly()
+ .areAllDynamic();
+
+ // Test for remove
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ mManager.removeDynamicShortcuts(list("s1"));
+ });
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("s2", "s3", "s4")
+ .areAllWithKeyFieldsOnly()
+ .areAllDynamic();
+
+ // Test for update
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.updateShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+ });
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("s2", "s3", "s4")
+ .areAllWithKeyFieldsOnly()
+ .areAllDynamic();
+
+ // Test for deleteAll
+ reset(c0);
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .isEmpty();
+
+ // Update package1 with manifest shortcuts
+ reset(c0);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .areAllManifest()
+ .areAllWithKeyFieldsOnly()
+ .haveIds("ms1", "ms2");
+
+ // Make sure pinned shortcuts are passed too.
+ // 1. Add dynamic shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+ });
+
+ // 2. Pin some.
+ runWithCaller(LAUNCHER_1, UserHandle.USER_SYSTEM, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2", "s2"), HANDLE_USER_0);
+ });
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "s1", "s2")
+ .areAllEnabled()
+
+ .selectByIds("ms1", "ms2")
+ .areAllManifest()
+
+ .revertToOriginalList()
+ .selectByIds("s1", "s2")
+ .areAllDynamic()
+ ;
+ });
+
+ // 3 Update the app with no manifest shortcuts. (Pinned one will survive.)
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_0);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ reset(c0); // Check the callback for the next API call.
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ mManager.removeDynamicShortcuts(list("s2"));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("ms2", "s1", "s2")
+
+ .selectByIds("ms2")
+ .areAllNotManifest()
+ .areAllPinned()
+ .areAllImmutable()
+ .areAllDisabled()
+
+ .revertToOriginalList()
+ .selectByIds("s1")
+ .areAllDynamic()
+ .areAllNotPinned()
+ .areAllEnabled()
+
+ .revertToOriginalList()
+ .selectByIds("s2")
+ .areAllNotDynamic()
+ .areAllPinned()
+ .areAllEnabled()
+ ;
+ });
+
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .haveIds("ms2", "s1", "s2")
+ .areAllWithKeyFieldsOnly();
+
+ // Remove CALLING_PACKAGE_2
+ reset(c0);
+ uninstallPackage(USER_0, CALLING_PACKAGE_2);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0);
+
+ // Should get a callback with an empty list.
+ waitOnMainThread();
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_2),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0)
+ );
+ assertWith(shortcuts.getValue())
+ .isEmpty();
+ }
+
+ public void testLauncherCallback_crossProfile() throws Throwable {
+ prepareCrossProfileDataSet();
+
+ final Handler h = new Handler(Looper.getMainLooper());
+
+ final LauncherApps.Callback c0_1 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c0_2 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c0_3 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c0_4 = mock(LauncherApps.Callback.class);
+
+ final LauncherApps.Callback cP0_1 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c10_1 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c10_2 = mock(LauncherApps.Callback.class);
+ final LauncherApps.Callback c11_1 = mock(LauncherApps.Callback.class);
+
+ final List<LauncherApps.Callback> all =
+ list(c0_1, c0_2, c0_3, c0_4, cP0_1, c10_1, c11_1);
+
+ setDefaultLauncherChecker((pkg, userId) -> {
+ switch (userId) {
+ case USER_0:
+ return LAUNCHER_2.equals(pkg);
+ case USER_P0:
+ return LAUNCHER_1.equals(pkg);
+ case USER_10:
+ return LAUNCHER_1.equals(pkg);
+ case USER_11:
+ return LAUNCHER_1.equals(pkg);
+ default:
+ return false;
+ }
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> mLauncherApps.registerCallback(c0_1, h));
+ runWithCaller(LAUNCHER_2, USER_0, () -> mLauncherApps.registerCallback(c0_2, h));
+ runWithCaller(LAUNCHER_3, USER_0, () -> mLauncherApps.registerCallback(c0_3, h));
+ runWithCaller(LAUNCHER_4, USER_0, () -> mLauncherApps.registerCallback(c0_4, h));
+ runWithCaller(LAUNCHER_1, USER_P0, () -> mLauncherApps.registerCallback(cP0_1, h));
+ runWithCaller(LAUNCHER_1, USER_10, () -> mLauncherApps.registerCallback(c10_1, h));
+ runWithCaller(LAUNCHER_2, USER_10, () -> mLauncherApps.registerCallback(c10_2, h));
+ runWithCaller(LAUNCHER_1, USER_11, () -> mLauncherApps.registerCallback(c11_1, h));
+
+ // User 0.
+
+ resetAll(all);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeDynamicShortcuts(list());
+ });
+ waitOnMainThread();
+
+ assertCallbackNotReceived(c0_1);
+ assertCallbackNotReceived(c0_3);
+ assertCallbackNotReceived(c0_4);
+ assertCallbackNotReceived(c10_1);
+ assertCallbackNotReceived(c10_2);
+ assertCallbackNotReceived(c11_1);
+ assertCallbackReceived(c0_2, HANDLE_USER_0, CALLING_PACKAGE_1, "s1", "s2", "s3");
+ assertCallbackReceived(cP0_1, HANDLE_USER_0, CALLING_PACKAGE_1, "s1", "s2", "s3", "s4");
+
+ // User 0, different package.
+
+ resetAll(all);
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ mManager.removeDynamicShortcuts(list());
+ });
+ waitOnMainThread();
+
+ assertCallbackNotReceived(c0_1);
+ assertCallbackNotReceived(c0_3);
+ assertCallbackNotReceived(c0_4);
+ assertCallbackNotReceived(c10_1);
+ assertCallbackNotReceived(c10_2);
+ assertCallbackNotReceived(c11_1);
+ assertCallbackReceived(c0_2, HANDLE_USER_0, CALLING_PACKAGE_3, "s1", "s2", "s3", "s4");
+ assertCallbackReceived(cP0_1, HANDLE_USER_0, CALLING_PACKAGE_3,
+ "s1", "s2", "s3", "s4", "s5", "s6");
+
+ // Work profile, but not running, so don't send notifications.
+
+ resetAll(all);
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ mManager.removeDynamicShortcuts(list());
+ });
+ waitOnMainThread();
+
+ assertCallbackNotReceived(c0_1);
+ assertCallbackNotReceived(c0_2);
+ assertCallbackNotReceived(c0_3);
+ assertCallbackNotReceived(c0_4);
+ assertCallbackNotReceived(cP0_1);
+ assertCallbackNotReceived(c10_1);
+ assertCallbackNotReceived(c10_2);
+ assertCallbackNotReceived(c11_1);
+
+ // Work profile, now running.
+ doAnswer(new AnswerIsUserRunning(false)).when(mMockUserManager).isUserRunning(anyInt());
+ doAnswer(new AnswerIsUserRunning(true)).when(mMockUserManager).isUserRunning(eq(USER_P0));
+
+ resetAll(all);
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ mManager.removeDynamicShortcuts(list());
+ });
+ waitOnMainThread();
+
+ assertCallbackNotReceived(c0_1);
+ assertCallbackNotReceived(c0_3);
+ assertCallbackNotReceived(c0_4);
+ assertCallbackNotReceived(c10_1);
+ assertCallbackNotReceived(c10_2);
+ assertCallbackNotReceived(c11_1);
+ assertCallbackReceived(c0_2, HANDLE_USER_P0, CALLING_PACKAGE_1, "s1", "s2", "s3", "s5");
+ assertCallbackReceived(cP0_1, HANDLE_USER_P0, CALLING_PACKAGE_1, "s1", "s2", "s3", "s4");
+
+ // Normal secondary user.
+
+ doAnswer(new AnswerIsUserRunning(false)).when(mMockUserManager).isUserRunning(anyInt());
+ doAnswer(new AnswerIsUserRunning(true)).when(mMockUserManager).isUserRunning(eq(USER_10));
+
+ resetAll(all);
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ mManager.removeDynamicShortcuts(list());
+ });
+ waitOnMainThread();
+
+ assertCallbackNotReceived(c0_1);
+ assertCallbackNotReceived(c0_2);
+ assertCallbackNotReceived(c0_3);
+ assertCallbackNotReceived(c0_4);
+ assertCallbackNotReceived(cP0_1);
+ assertCallbackNotReceived(c10_2);
+ assertCallbackNotReceived(c11_1);
+ assertCallbackReceived(c10_1, HANDLE_USER_10, CALLING_PACKAGE_1,
+ "x1", "x2", "x3", "x4", "x5");
+ }
+
+ // === Test for persisting ===
+
+ public void testSaveAndLoadUser_empty() {
+ assertTrue(mManager.setDynamicShortcuts(list()));
+
+ Log.i(TAG, "Saved state");
+ dumpsysOnLogcat();
+ dumpUserFile(0);
+
+ // Restore.
+ mService.saveDirtyInfo();
+ initService();
+
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ }
+
+ /**
+ * Try save and load, also stop/start the user.
+ */
+ public void testSaveAndLoadUser() {
+ // First, create some shortcuts and save.
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16);
+ final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.icon2));
+
+ final ShortcutInfo si1 = makeShortcut(
+ "s1",
+ "title1-1",
+ makeComponent(ShortcutActivity.class),
+ icon1,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
+ "key1", "val1", "nest", makeBundle("key", 123)),
+ /* weight */ 10);
+
+ final ShortcutInfo si2 = makeShortcut(
+ "s2",
+ "title1-2",
+ /* activity */ null,
+ icon2,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
+ /* weight */ 12);
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
+
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+ assertEquals(2, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_16x64);
+ final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.icon2));
+
+ final ShortcutInfo si1 = makeShortcut(
+ "s1",
+ "title2-1",
+ makeComponent(ShortcutActivity.class),
+ icon1,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
+ "key1", "val1", "nest", makeBundle("key", 123)),
+ /* weight */ 10);
+
+ final ShortcutInfo si2 = makeShortcut(
+ "s2",
+ "title2-2",
+ /* activity */ null,
+ icon2,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
+ /* weight */ 12);
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
+
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+ assertEquals(2, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64);
+ final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.icon2));
+
+ final ShortcutInfo si1 = makeShortcut(
+ "s1",
+ "title10-1-1",
+ makeComponent(ShortcutActivity.class),
+ icon1,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
+ "key1", "val1", "nest", makeBundle("key", 123)),
+ /* weight */ 10);
+
+ final ShortcutInfo si2 = makeShortcut(
+ "s2",
+ "title10-1-2",
+ /* activity */ null,
+ icon2,
+ makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
+ /* weight */ 12);
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
+
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+ assertEquals(2, mManager.getRemainingCallCount());
+ });
+
+ mService.getShortcutsForTest().get(UserHandle.USER_SYSTEM).setDefaultLauncherComponent(
+ new ComponentName("pkg1", "class"));
+
+ // Restore.
+ mService.saveDirtyInfo();
+ initService();
+
+ // Before the load, the map should be empty.
+ assertEquals(0, mService.getShortcutsForTest().size());
+
+ // this will pre-load the per-user info.
+ mService.handleUnlockUser(UserHandle.USER_SYSTEM);
+
+ // Now it's loaded.
+ assertEquals(1, mService.getShortcutsForTest().size());
+
+ runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
+ mManager.getDynamicShortcuts()))), "s1", "s2");
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ assertEquals("title1-1", getCallerShortcut("s1").getTitle());
+ assertEquals("title1-2", getCallerShortcut("s2").getTitle());
+ });
+ runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
+ assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
+ mManager.getDynamicShortcuts()))), "s1", "s2");
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ assertEquals("title2-1", getCallerShortcut("s1").getTitle());
+ assertEquals("title2-2", getCallerShortcut("s2").getTitle());
+ });
+
+ assertEquals("pkg1", mService.getShortcutsForTest().get(UserHandle.USER_SYSTEM)
+ .getDefaultLauncherComponent().getPackageName());
+
+ // Start another user
+ mService.handleUnlockUser(USER_10);
+
+ // Now the size is 2.
+ assertEquals(2, mService.getShortcutsForTest().size());
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
+ mManager.getDynamicShortcuts()))), "s1", "s2");
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ assertEquals("title10-1-1", getCallerShortcut("s1").getTitle());
+ assertEquals("title10-1-2", getCallerShortcut("s2").getTitle());
+ });
+ assertNull(mService.getShortcutsForTest().get(USER_10).getDefaultLauncherComponent());
+
+ // Try stopping the user
+ mService.handleCleanupUser(USER_10);
+
+ // Now it's unloaded.
+ assertEquals(1, mService.getShortcutsForTest().size());
+
+ // TODO Check all other fields
+ }
+
+ public void testCleanupPackage() {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s0_1"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s0_2"))));
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s0_1"),
+ HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s0_2"),
+ HANDLE_USER_0);
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s0_1"),
+ HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s0_2"),
+ HANDLE_USER_0);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s10_1"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s10_2"))));
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s10_1"),
+ HANDLE_USER_10);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s10_2"),
+ HANDLE_USER_10);
+ });
+ runWithCaller(LAUNCHER_2, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s10_1"),
+ HANDLE_USER_10);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s10_2"),
+ HANDLE_USER_10);
+ });
+
+ // Remove all dynamic shortcuts; now all shortcuts are just pinned.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+
+
+ final SparseArray<ShortcutUser> users = mService.getShortcutsForTest();
+ assertEquals(2, users.size());
+ assertEquals(USER_0, users.keyAt(0));
+ assertEquals(USER_10, users.keyAt(1));
+
+ final ShortcutUser user0 = users.get(USER_0);
+ final ShortcutUser user10 = users.get(USER_10);
+
+
+ // Check the registered packages.
+ dumpsysOnLogcat();
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_10, LAUNCHER_1),
+ PackageWithUser.of(USER_10, LAUNCHER_2)),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_1", "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_1", "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // Nonexistent package.
+ uninstallPackage(USER_0, "abc");
+ mService.cleanUpPackageLocked("abc", USER_0, USER_0);
+
+ // No changes.
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_10, LAUNCHER_1),
+ PackageWithUser.of(USER_10, LAUNCHER_2)),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_1", "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_1", "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // Remove a package.
+ uninstallPackage(USER_0, CALLING_PACKAGE_1);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0);
+
+ assertEquals(set(CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_10, LAUNCHER_1),
+ PackageWithUser.of(USER_10, LAUNCHER_2)),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // Remove a launcher.
+ uninstallPackage(USER_10, LAUNCHER_1);
+ mService.cleanUpPackageLocked(LAUNCHER_1, USER_10, USER_10);
+
+ assertEquals(set(CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_10),
+ "s10_1", "s10_2");
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // Remove a package.
+ uninstallPackage(USER_10, CALLING_PACKAGE_2);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_10, USER_10);
+
+ assertEquals(set(CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_10),
+ "s10_1");
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutNotExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // Remove the other launcher from user 10 too.
+ uninstallPackage(USER_10, LAUNCHER_2);
+ mService.cleanUpPackageLocked(LAUNCHER_2, USER_10, USER_10);
+
+ assertEquals(set(CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(CALLING_PACKAGE_1),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(
+ set(),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_2");
+
+ // Note the pinned shortcuts on user-10 no longer referred, so they should both be removed.
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutNotExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+
+ // More remove.
+ uninstallPackage(USER_10, CALLING_PACKAGE_1);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_10, USER_10);
+
+ assertEquals(set(CALLING_PACKAGE_2),
+ hashSet(user0.getAllPackagesForTest().keySet()));
+ assertEquals(set(),
+ hashSet(user10.getAllPackagesForTest().keySet()));
+ assertEquals(
+ set(PackageWithUser.of(USER_0, LAUNCHER_1),
+ PackageWithUser.of(USER_0, LAUNCHER_2)),
+ hashSet(user0.getAllLaunchersForTest().keySet()));
+ assertEquals(set(),
+ hashSet(user10.getAllLaunchersForTest().keySet()));
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
+ "s0_2");
+ assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_2, USER_0),
+ "s0_2");
+
+ // Note the pinned shortcuts on user-10 no longer referred, so they should both be removed.
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s0_1", USER_0);
+ assertShortcutExists(CALLING_PACKAGE_2, "s0_2", USER_0);
+ assertShortcutNotExists(CALLING_PACKAGE_1, "s10_1", USER_10);
+ assertShortcutNotExists(CALLING_PACKAGE_2, "s10_2", USER_10);
+
+ mService.saveDirtyInfo();
+ }
+
+ public void testHandleGonePackage_crossProfile() {
+ // Create some shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ // Pin some.
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2"), UserHandle.of(USER_P0));
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s3"), HANDLE_USER_0);
+ });
+
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2"), HANDLE_USER_0);
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s3"), UserHandle.of(USER_P0));
+
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2,
+ list("s1"), HANDLE_USER_0);
+ });
+
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s3"), HANDLE_USER_10);
+ });
+
+ // Check the state.
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ // Make sure all the information is persisted.
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_P0);
+ mService.handleUnlockUser(USER_10);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ // Start uninstalling.
+ uninstallPackage(USER_10, LAUNCHER_1);
+ mService.checkPackageChanges(USER_10);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ // Uninstall.
+ uninstallPackage(USER_10, CALLING_PACKAGE_1);
+ mService.checkPackageChanges(USER_10);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ uninstallPackage(USER_P0, LAUNCHER_1);
+ mService.checkPackageChanges(USER_0);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ mService.checkPackageChanges(USER_P0);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ uninstallPackage(USER_P0, CALLING_PACKAGE_1);
+
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_P0);
+ mService.handleUnlockUser(USER_10);
+
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ // Uninstall
+ uninstallPackage(USER_0, LAUNCHER_1);
+
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_P0);
+ mService.handleUnlockUser(USER_10);
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+
+ uninstallPackage(USER_0, CALLING_PACKAGE_2);
+
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_P0);
+ mService.handleUnlockUser(USER_10);
+
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
+ assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_P0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_P0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_2, "s2", USER_0));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_2, "s3", USER_0));
+
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
+ assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
+ }
+
+ protected void checkCanRestoreTo(boolean expected, ShortcutPackageInfo spi,
+ int version, String... signatures) {
+ assertEquals(expected, spi.canRestoreTo(mService, genPackage(
+ "dummy", /* uid */ 0, version, signatures)));
+ }
+
+ public void testCanRestoreTo() {
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sig1");
+ addPackage(CALLING_PACKAGE_2, CALLING_UID_1, 10, "sig1", "sig2");
+
+ final ShortcutPackageInfo spi1 = ShortcutPackageInfo.generateForInstalledPackage(
+ mService, CALLING_PACKAGE_1, USER_0);
+ final ShortcutPackageInfo spi2 = ShortcutPackageInfo.generateForInstalledPackage(
+ mService, CALLING_PACKAGE_2, USER_0);
+
+ checkCanRestoreTo(true, spi1, 10, "sig1");
+ checkCanRestoreTo(true, spi1, 10, "x", "sig1");
+ checkCanRestoreTo(true, spi1, 10, "sig1", "y");
+ checkCanRestoreTo(true, spi1, 10, "x", "sig1", "y");
+ checkCanRestoreTo(true, spi1, 11, "sig1");
+
+ checkCanRestoreTo(false, spi1, 10 /* empty */);
+ checkCanRestoreTo(false, spi1, 10, "x");
+ checkCanRestoreTo(false, spi1, 10, "x", "y");
+ checkCanRestoreTo(false, spi1, 10, "x");
+ checkCanRestoreTo(false, spi1, 9, "sig1");
+
+ checkCanRestoreTo(true, spi2, 10, "sig1", "sig2");
+ checkCanRestoreTo(true, spi2, 10, "sig2", "sig1");
+ checkCanRestoreTo(true, spi2, 10, "x", "sig1", "sig2");
+ checkCanRestoreTo(true, spi2, 10, "x", "sig2", "sig1");
+ checkCanRestoreTo(true, spi2, 10, "sig1", "sig2", "y");
+ checkCanRestoreTo(true, spi2, 10, "sig2", "sig1", "y");
+ checkCanRestoreTo(true, spi2, 10, "x", "sig1", "sig2", "y");
+ checkCanRestoreTo(true, spi2, 10, "x", "sig2", "sig1", "y");
+ checkCanRestoreTo(true, spi2, 11, "x", "sig2", "sig1", "y");
+
+ checkCanRestoreTo(false, spi2, 10, "sig1", "sig2x");
+ checkCanRestoreTo(false, spi2, 10, "sig2", "sig1x");
+ checkCanRestoreTo(false, spi2, 10, "x", "sig1x", "sig2");
+ checkCanRestoreTo(false, spi2, 10, "x", "sig2x", "sig1");
+ checkCanRestoreTo(false, spi2, 10, "sig1", "sig2x", "y");
+ checkCanRestoreTo(false, spi2, 10, "sig2", "sig1x", "y");
+ checkCanRestoreTo(false, spi2, 10, "x", "sig1x", "sig2", "y");
+ checkCanRestoreTo(false, spi2, 10, "x", "sig2x", "sig1", "y");
+ checkCanRestoreTo(false, spi2, 11, "x", "sig2x", "sig1", "y");
+ }
+
+ public void testHandlePackageDelete() {
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+ setCaller(CALLING_PACKAGE_1, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", bmp32x32), makeShortcutWithIcon("s2", bmp32x32)
+ )));
+
+ setCaller(CALLING_PACKAGE_2, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_3, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_1, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_2, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_3, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ uninstallPackage(USER_0, CALLING_PACKAGE_1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageDeleteIntent(CALLING_PACKAGE_1, USER_0));
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ uninstallPackage(USER_10, CALLING_PACKAGE_2);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageDeleteIntent(CALLING_PACKAGE_2, USER_10));
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ mInjectedPackages.remove(CALLING_PACKAGE_1);
+ mInjectedPackages.remove(CALLING_PACKAGE_3);
+
+ mService.handleUnlockUser(USER_0);
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ mService.handleUnlockUser(USER_10);
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+ }
+
+ /** Almost ame as testHandlePackageDelete, except it doesn't uninstall packages. */
+ public void testHandlePackageClearData() {
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+ setCaller(CALLING_PACKAGE_1, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", bmp32x32), makeShortcutWithIcon("s2", bmp32x32)
+ )));
+
+ setCaller(CALLING_PACKAGE_2, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_3, USER_0);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_1, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_2, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ setCaller(CALLING_PACKAGE_3, USER_10);
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
+
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageDataClear(CALLING_PACKAGE_1, USER_0));
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageDataClear(CALLING_PACKAGE_2, USER_10));
+
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_0));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "s1", USER_10));
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_2, "s1", USER_10));
+ assertNotNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_3, "s1", USER_10));
+
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_0));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_1, USER_10));
+ assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_2, USER_10));
+ assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
+ }
+
+ public void testHandlePackageUpdate() throws Throwable {
+
+ // Set up shortcuts and launchers.
+
+ final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"),
+ makeShortcutWithIcon("s2", res32x32),
+ makeShortcutWithIcon("s3", res32x32),
+ makeShortcutWithIcon("s4", bmp32x32))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"),
+ makeShortcutWithIcon("s2", bmp32x32))));
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", res32x32))));
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", res32x32),
+ makeShortcutWithIcon("s2", res32x32))));
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithIcon("s1", bmp32x32),
+ makeShortcutWithIcon("s2", bmp32x32))));
+ });
+
+ LauncherApps.Callback c0 = mock(LauncherApps.Callback.class);
+ LauncherApps.Callback c10 = mock(LauncherApps.Callback.class);
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper()));
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.registerCallback(c10, new Handler(Looper.getMainLooper()));
+ });
+
+ mInjectedCurrentTimeMillis = START_TIME + 100;
+
+ ArgumentCaptor<List> shortcuts;
+
+ // First, call the event without updating the versions.
+ reset(c0);
+ reset(c10);
+
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_1, USER_10));
+
+ waitOnMainThread();
+
+ // Version not changed, so no callback.
+ verify(c0, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+ verify(c10, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+
+ // Next, update the version info for package 1.
+ reset(c0);
+ reset(c10);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+
+ // Then send the broadcast, to only user-0.
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+
+ waitOnMainThread();
+
+ // User-0 should get the notification.
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0));
+
+ // User-10 shouldn't yet get the notification.
+ verify(c10, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+ assertShortcutIds(shortcuts.getValue(), "s1", "s2", "s3", "s4");
+ assertEquals(START_TIME,
+ findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+ assertEquals(START_TIME + 100,
+ findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp());
+ assertEquals(START_TIME + 100,
+ findShortcut(shortcuts.getValue(), "s3").getLastChangedTimestamp());
+ assertEquals(START_TIME,
+ findShortcut(shortcuts.getValue(), "s4").getLastChangedTimestamp());
+
+ // Next, send unlock even on user-10. Now we scan packages on this user and send a
+ // notification to the launcher.
+ mInjectedCurrentTimeMillis = START_TIME + 200;
+
+ doAnswer(new AnswerIsUserRunning(true)).when(mMockUserManager).isUserRunning(eq(USER_10));
+
+ reset(c0);
+ reset(c10);
+ mService.handleUnlockUser(USER_10);
+
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+
+ verify(c10).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ shortcuts.capture(),
+ eq(HANDLE_USER_10));
+
+ assertShortcutIds(shortcuts.getValue(), "s1", "s2");
+ assertEquals(START_TIME + 200,
+ findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+ assertEquals(START_TIME + 200,
+ findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp());
+
+
+ // Do the same thing for package 2, which doesn't have resource icons.
+ mInjectedCurrentTimeMillis = START_TIME + 300;
+
+ reset(c0);
+ reset(c10);
+ updatePackageVersion(CALLING_PACKAGE_2, 10);
+
+ // Then send the broadcast, to only user-0.
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_2, USER_0));
+ mService.handleUnlockUser(USER_10);
+
+ waitOnMainThread();
+
+ verify(c0, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+
+ verify(c10, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_1),
+ any(List.class),
+ any(UserHandle.class));
+
+ // Do the same thing for package 3
+ mInjectedCurrentTimeMillis = START_TIME + 400;
+
+ reset(c0);
+ reset(c10);
+ updatePackageVersion(CALLING_PACKAGE_3, 100);
+
+ // Then send the broadcast, to only user-0.
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_3, USER_0));
+ mService.handleUnlockUser(USER_10);
+
+ waitOnMainThread();
+
+ shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(c0).onShortcutsChanged(
+ eq(CALLING_PACKAGE_3),
+ shortcuts.capture(),
+ eq(HANDLE_USER_0));
+
+ // User 10 doesn't have package 3, so no callback.
+ verify(c10, times(0)).onShortcutsChanged(
+ eq(CALLING_PACKAGE_3),
+ any(List.class),
+ any(UserHandle.class));
+
+ assertShortcutIds(shortcuts.getValue(), "s1");
+ assertEquals(START_TIME + 400,
+ findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+ }
+
+ /**
+ * Test the case where an updated app has resource IDs changed.
+ */
+ public void testHandlePackageUpdate_resIdChanged() throws Exception {
+ final Icon icon1 = Icon.createWithResource(getTestContext(), /* res ID */ 1000);
+ final Icon icon2 = Icon.createWithResource(getTestContext(), /* res ID */ 1001);
+
+ // Set up shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // Note resource strings are not officially supported (they're hidden), but
+ // should work.
+
+ final ShortcutInfo s1 = new ShortcutInfo.Builder(mClientContext)
+ .setId("s1")
+ .setActivity(makeComponent(ShortcutActivity.class))
+ .setIntent(new Intent(Intent.ACTION_VIEW))
+ .setIcon(icon1)
+ .setTitleResId(10000)
+ .setTextResId(10001)
+ .setDisabledMessageResId(10002)
+ .build();
+
+ final ShortcutInfo s2 = new ShortcutInfo.Builder(mClientContext)
+ .setId("s2")
+ .setActivity(makeComponent(ShortcutActivity.class))
+ .setIntent(new Intent(Intent.ACTION_VIEW))
+ .setIcon(icon2)
+ .setTitleResId(20000)
+ .build();
+
+ assertTrue(mManager.setDynamicShortcuts(list(s1, s2)));
+ });
+
+ // Verify.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ShortcutInfo s1 = getCallerShortcut("s1");
+ final ShortcutInfo s2 = getCallerShortcut("s2");
+
+ assertEquals(1000, s1.getIconResourceId());
+ assertEquals(10000, s1.getTitleResId());
+ assertEquals(10001, s1.getTextResId());
+ assertEquals(10002, s1.getDisabledMessageResourceId());
+
+ assertEquals(1001, s2.getIconResourceId());
+ assertEquals(20000, s2.getTitleResId());
+ assertEquals(0, s2.getTextResId());
+ assertEquals(0, s2.getDisabledMessageResourceId());
+ });
+
+ mService.saveDirtyInfo();
+ initService();
+
+ // Set up the mock resources again, with an "adjustment".
+ // When the package is updated, the service will fetch the updated res-IDs with res-names,
+ // and the new IDs will have this offset.
+ setUpAppResources(10);
+
+ // Update the package.
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ShortcutInfo s1 = getCallerShortcut("s1");
+ final ShortcutInfo s2 = getCallerShortcut("s2");
+
+ assertEquals(1010, s1.getIconResourceId());
+ assertEquals(10010, s1.getTitleResId());
+ assertEquals(10011, s1.getTextResId());
+ assertEquals(10012, s1.getDisabledMessageResourceId());
+
+ assertEquals(1011, s2.getIconResourceId());
+ assertEquals(20010, s2.getTitleResId());
+ assertEquals(0, s2.getTextResId());
+ assertEquals(0, s2.getDisabledMessageResourceId());
+ });
+ }
+
+ protected void prepareForBackupTest() {
+
+ prepareCrossProfileDataSet();
+
+ backupAndRestore();
+ }
+
+ /**
+ * Make sure the backup data doesn't have the following information:
+ * - Launchers on other users.
+ * - Non-backup app information.
+ *
+ * But restores all other infomation.
+ *
+ * It also omits the following pieces of information, but that's tested in
+ * {@link ShortcutManagerTest2#testShortcutInfoSaveAndLoad_forBackup}.
+ * - Unpinned dynamic shortcuts
+ * - Bitmaps
+ */
+ public void testBackupAndRestore() {
+ prepareForBackupTest();
+
+ checkBackupAndRestore_success();
+ }
+
+ public void testBackupAndRestore_backupRestoreTwice() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ dumpsysOnLogcat("Before second backup");
+
+ backupAndRestore();
+
+ dumpsysOnLogcat("After second backup");
+
+ checkBackupAndRestore_success();
+ }
+
+ public void testBackupAndRestore_backupRestoreMultiple() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ // This also shouldn't affect the result.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"),
+ makeShortcut("s4"), makeShortcut("s5"), makeShortcut("s6"))));
+ });
+
+ backupAndRestore();
+
+ checkBackupAndRestore_success();
+ }
+
+ public void testBackupAndRestore_restoreToNewVersion() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 2);
+ addPackage(LAUNCHER_1, LAUNCHER_UID_1, 5);
+
+ checkBackupAndRestore_success();
+ }
+
+ public void testBackupAndRestore_restoreToSuperSetSignatures() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ // Change package signatures.
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 1, "sigx", CALLING_PACKAGE_1);
+ addPackage(LAUNCHER_1, LAUNCHER_UID_1, 4, LAUNCHER_1, "sigy");
+
+ checkBackupAndRestore_success();
+ }
+
+ protected void checkBackupAndRestore_success() {
+ // Make sure non-system user is not restored.
+ final ShortcutUser userP0 = mService.getUserShortcutsLocked(USER_P0);
+ assertEquals(0, userP0.getAllPackagesForTest().size());
+ assertEquals(0, userP0.getAllLaunchersForTest().size());
+
+ // Make sure only "allowBackup" apps are restored, and are shadow.
+ final ShortcutUser user0 = mService.getUserShortcutsLocked(USER_0);
+ assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_1));
+ assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_2));
+ assertExistsAndShadow(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_1)));
+ assertExistsAndShadow(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_2)));
+
+ assertNull(user0.getAllPackagesForTest().get(CALLING_PACKAGE_3));
+ assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_3)));
+ assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_P0, LAUNCHER_1)));
+
+ installPackage(USER_0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2");
+ });
+
+ installPackage(USER_0, LAUNCHER_1);
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0))
+ /* empty, not restored */ );
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty, not restored */ );
+
+ assertEquals(0, mLauncherApps.getShortcuts(QUERY_ALL, HANDLE_USER_P0).size());
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_2);
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3");
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s1", "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty, not restored */ );
+
+ assertEquals(0, mLauncherApps.getShortcuts(QUERY_ALL, HANDLE_USER_P0).size());
+ });
+
+ // 3 shouldn't be backed up, so no pinned shortcuts.
+ installPackage(USER_0, CALLING_PACKAGE_3);
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ // Launcher on a different profile shouldn't be restored.
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)
+ .size());
+ assertEquals(0,
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)
+ .size());
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* wasn't restored, so still empty */ );
+ });
+
+ // Package on a different profile, no restore.
+ installPackage(USER_P0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ // Restore launcher 2 on user 0.
+ installPackage(USER_0, LAUNCHER_2);
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* wasn't restored, so still empty */ );
+
+ assertEquals(0, mLauncherApps.getShortcuts(QUERY_ALL, HANDLE_USER_P0).size());
+ });
+
+
+ // Restoration of launcher2 shouldn't affect other packages; so do the same checks and
+ // make sure they still have the same result.
+ installPackage(USER_0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2");
+ });
+
+ installPackage(USER_0, LAUNCHER_1);
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s1");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s1", "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* wasn't restored, so still empty */ );
+
+ assertEquals(0, mLauncherApps.getShortcuts(QUERY_ALL, HANDLE_USER_P0).size());
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_2);
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3");
+ });
+ }
+
+ public void testBackupAndRestore_publisherLowerVersion() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 0); // Lower version
+
+ checkBackupAndRestore_publisherNotRestored();
+ }
+
+ public void testBackupAndRestore_publisherWrongSignature() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sigx"); // different signature
+
+ checkBackupAndRestore_publisherNotRestored();
+ }
+
+ public void testBackupAndRestore_publisherNoLongerBackupTarget() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ updatePackageInfo(CALLING_PACKAGE_1,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+
+ checkBackupAndRestore_publisherNotRestored();
+ }
+
+ protected void checkBackupAndRestore_publisherNotRestored() {
+ installPackage(USER_0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_2);
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3");
+ });
+
+ installPackage(USER_0, LAUNCHER_1);
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s1", "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ installPackage(USER_0, LAUNCHER_2);
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_3);
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s1", "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ }
+
+ public void testBackupAndRestore_launcherLowerVersion() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ addPackage(LAUNCHER_1, LAUNCHER_UID_1, 0); // Lower version
+
+ checkBackupAndRestore_launcherNotRestored();
+ }
+
+ public void testBackupAndRestore_launcherWrongSignature() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ addPackage(LAUNCHER_1, LAUNCHER_UID_1, 10, "sigx"); // different signature
+
+ checkBackupAndRestore_launcherNotRestored();
+ }
+
+ public void testBackupAndRestore_launcherNoLongerBackupTarget() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ updatePackageInfo(LAUNCHER_1,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+
+ checkBackupAndRestore_launcherNotRestored();
+ }
+
+ protected void checkBackupAndRestore_launcherNotRestored() {
+ installPackage(USER_0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+
+ // s1 was pinned by launcher 1, which is not restored, yet, so we still see "s1" here.
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2");
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_2);
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3");
+ });
+
+ // Now we try to restore launcher 1. Then we realize it's not restorable, so L1 has no pinned
+ // shortcuts.
+ installPackage(USER_0, LAUNCHER_1);
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+
+ // Now CALLING_PACKAGE_1 realizes "s1" is no longer pinned.
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s2");
+ });
+
+ installPackage(USER_0, LAUNCHER_2);
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_3);
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0)),
+ "s2");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ }
+
+ public void testBackupAndRestore_launcherAndPackageNoLongerBackupTarget() {
+ prepareForBackupTest();
+
+ // Note doing a backup & restore again here shouldn't affect the result.
+ backupAndRestore();
+
+ updatePackageInfo(CALLING_PACKAGE_1,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+
+ updatePackageInfo(LAUNCHER_1,
+ pi -> pi.applicationInfo.flags &= ~ApplicationInfo.FLAG_ALLOW_BACKUP);
+
+ checkBackupAndRestore_publisherAndLauncherNotRestored();
+ }
+
+ protected void checkBackupAndRestore_publisherAndLauncherNotRestored() {
+ installPackage(USER_0, CALLING_PACKAGE_1);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_2);
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3");
+ });
+
+ installPackage(USER_0, LAUNCHER_1);
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ installPackage(USER_0, LAUNCHER_2);
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+
+ // Because launcher 1 wasn't restored, "s1" is no longer pinned.
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertShortcutIds(assertAllPinned(
+ mManager.getPinnedShortcuts()),
+ "s2", "s3");
+ });
+
+ installPackage(USER_0, CALLING_PACKAGE_3);
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(0, mManager.getDynamicShortcuts().size());
+ assertEquals(0, mManager.getPinnedShortcuts().size());
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0))
+ /* empty */);
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_2), HANDLE_USER_0)),
+ "s2", "s3");
+ assertShortcutIds(assertAllPinned(
+ mLauncherApps.getShortcuts(buildAllQuery(CALLING_PACKAGE_3), HANDLE_USER_0))
+ /* empty */);
+ });
+ }
+
+ public void testSaveAndLoad_crossProfile() {
+ prepareCrossProfileDataSet();
+
+ dumpsysOnLogcat("Before save & load");
+
+ mService.saveDirtyInfo();
+ initService();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3", "s4");
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3", "s4", "s5");
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3", "s4", "s5", "s6");
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts())
+ /* empty */);
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts())
+ /* empty */);
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()),
+ "s1", "s2", "s3");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()),
+ "s1", "s2", "s3", "s4", "s5", "s6");
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_P0, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts())
+ /* empty */);
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts())
+ /* empty */);
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertShortcutIds(assertAllDynamic(mManager.getDynamicShortcuts()),
+ "x1", "x2", "x3");
+ assertShortcutIds(assertAllPinned(mManager.getPinnedShortcuts()),
+ "x4", "x5");
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_0),
+ "s1");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_0),
+ "s1", "s2");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_0),
+ "s1", "s2", "s3");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_4), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_P0),
+ "s1", "s4");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_P0)
+ /* empty */);
+ assertExpectException(
+ SecurityException.class, "", () -> {
+ mLauncherApps.getShortcuts(
+ buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_10);
+ });
+ });
+ runWithCaller(LAUNCHER_2, USER_0, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_0),
+ "s2");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_0),
+ "s2", "s3");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_0),
+ "s2", "s3", "s4");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_4), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_P0),
+ "s2", "s5");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_P0)
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_3, USER_0, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_0),
+ "s3");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_0),
+ "s3", "s4");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_0),
+ "s3", "s4", "s5");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_4), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_P0),
+ "s3", "s6");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_P0)
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_4, USER_0, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_4), HANDLE_USER_0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_P0)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_P0)
+ /* empty */);
+ });
+ runWithCaller(LAUNCHER_1, USER_P0, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_0),
+ "s3", "s4");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_0),
+ "s3", "s4", "s5");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_0),
+ "s3", "s4", "s5", "s6");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_P0),
+ "s1", "s4");
+ assertExpectException(
+ SecurityException.class, "unrelated profile", () -> {
+ mLauncherApps.getShortcuts(
+ buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_10);
+ });
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_1), HANDLE_USER_10),
+ "x4", "x5");
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_2), HANDLE_USER_10)
+ /* empty */);
+ assertShortcutIds(
+ mLauncherApps.getShortcuts(buildPinnedQuery(CALLING_PACKAGE_3), HANDLE_USER_10)
+ /* empty */);
+ assertExpectException(
+ SecurityException.class, "unrelated profile", () -> {
+ mLauncherApps.getShortcuts(
+ buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_0);
+ });
+ assertExpectException(
+ SecurityException.class, "unrelated profile", () -> {
+ mLauncherApps.getShortcuts(
+ buildAllQuery(CALLING_PACKAGE_1), HANDLE_USER_P0);
+ });
+ });
+ }
+
+ public void testOnApplicationActive_permission() {
+ assertExpectException(SecurityException.class, "Missing permission", () ->
+ mManager.onApplicationActive(CALLING_PACKAGE_1, USER_0));
+
+ // Has permission, now it should pass.
+ mCallerPermissions.add(permission.RESET_SHORTCUT_MANAGER_THROTTLING);
+ mManager.onApplicationActive(CALLING_PACKAGE_1, USER_0);
+ }
+
+ public void testDumpsys_crossProfile() {
+ prepareCrossProfileDataSet();
+ dumpsysOnLogcat("test1", /* force= */ true);
+ }
+
+ public void testDumpsys_withIcons() throws IOException {
+ testIcons();
+ // Dump after having some icons.
+ dumpsysOnLogcat("test1", /* force= */ true);
+ }
+
+ public void testManifestShortcut_publishOnUnlockUser() {
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+
+ // Unlock user-0.
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Try on another user, with some packages uninstalled.
+ uninstallPackage(USER_10, CALLING_PACKAGE_1);
+ uninstallPackage(USER_10, CALLING_PACKAGE_3);
+
+ mService.handleUnlockUser(USER_10);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_10, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Now change the resources for package 1, and unlock again.
+ // But we still see *old* shortcuts, because the package version and install time
+ // hasn't changed.
+ shutdownServices();
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+
+ initService();
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Do it again, but this time we change the app version, so we do detect the changes.
+ shutdownServices();
+
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ updatePackageLastUpdateTime(CALLING_PACKAGE_3, 1);
+
+ initService();
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Next, try removing all shortcuts, with some of them pinned.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms3"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("ms2"), HANDLE_USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("ms1"), HANDLE_USER_0);
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest(
+ assertAllEnabled(mManager.getPinnedShortcuts())))),
+ "ms3");
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest(
+ assertAllEnabled(mManager.getPinnedShortcuts())))),
+ "ms2");
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest(
+ assertAllEnabled(mManager.getPinnedShortcuts())))),
+ "ms1");
+ });
+
+ shutdownServices();
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_0);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()),
+ R.xml.shortcut_0);
+
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ updatePackageVersion(CALLING_PACKAGE_2, 1);
+ updatePackageVersion(CALLING_PACKAGE_3, 1);
+
+ initService();
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest(
+ assertAllDisabled(mManager.getPinnedShortcuts())))),
+ "ms3");
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest(
+ assertAllDisabled(mManager.getPinnedShortcuts())))),
+ "ms2");
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest(
+ assertAllDisabled(mManager.getPinnedShortcuts())))),
+ "ms1");
+ });
+
+ // Make sure we don't have ShortcutPackage for packages that don't have shortcuts.
+ assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_4, USER_0));
+ assertNull(mService.getPackageShortcutForTest(LAUNCHER_1, USER_0));
+ }
+
+ public void testManifestShortcut_publishOnBroadcast() {
+ // First, no packages are installed.
+ uninstallPackage(USER_0, CALLING_PACKAGE_1);
+ uninstallPackage(USER_0, CALLING_PACKAGE_2);
+ uninstallPackage(USER_0, CALLING_PACKAGE_3);
+ uninstallPackage(USER_0, CALLING_PACKAGE_4);
+ uninstallPackage(USER_10, CALLING_PACKAGE_1);
+ uninstallPackage(USER_10, CALLING_PACKAGE_2);
+ uninstallPackage(USER_10, CALLING_PACKAGE_3);
+ uninstallPackage(USER_10, CALLING_PACKAGE_4);
+
+ mService.handleUnlockUser(USER_0);
+ mService.handleUnlockUser(USER_10);
+
+ // Originally no manifest shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Package 1 updated, with manifest shortcuts.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Package 2 updated, with manifest shortcuts.
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_2, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_2, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1", "ms2", "ms3", "ms4", "ms5");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // Package 2 updated, with less manifest shortcuts.
+ // This time we use updatePackageLastUpdateTime() instead of updatePackageVersion().
+
+ dumpsysOnLogcat("Before pinning");
+
+ // Also pin some.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("ms2", "ms3"), HANDLE_USER_0);
+ });
+
+ dumpsysOnLogcat("After pinning");
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_2, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1", "ms2");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(
+ mManager.getPinnedShortcuts())),
+ "ms2", "ms3");
+ // ms3 is no longer in manifest, so should be disabled.
+ // but ms1 and ms2 should be enabled.
+ assertAllEnabled(list(getCallerShortcut("ms1")));
+ assertAllEnabled(list(getCallerShortcut("ms2")));
+ assertAllDisabled(list(getCallerShortcut("ms3")));
+ });
+
+ // Package 2 on user 10 has no shortcuts yet.
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+ // Send PACKAGE_ADD broadcast to have Package 2 on user-10 publish manifest shortcuts.
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_2, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1", "ms2");
+ assertEmpty(mManager.getPinnedShortcuts());
+ });
+
+ // But it shouldn't affect user-0.
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1", "ms2");
+ assertShortcutIds(assertAllImmutable(assertAllPinned(
+ mManager.getPinnedShortcuts())),
+ "ms2", "ms3");
+ assertAllEnabled(list(getCallerShortcut("ms1")));
+ assertAllEnabled(list(getCallerShortcut("ms2")));
+ assertAllDisabled(list(getCallerShortcut("ms3")));
+ });
+
+ // Multiple activities.
+ // Add shortcuts on activity 2 for package 2.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5_alt);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_5_reverse);
+
+ updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_2, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5",
+ "ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt");
+
+ // Make sure they have the correct ranks, regardless of their ID's alphabetical order.
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName()))
+ .haveRanksInOrder("ms5", "ms4", "ms3", "ms2", "ms1");
+ });
+
+ // Package 2 now has no manifest shortcuts.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()),
+ R.xml.shortcut_0);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_0);
+ updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_2, USER_0));
+
+ // No manifest shortcuts, and pinned ones are disabled.
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllDisabled(
+ mManager.getPinnedShortcuts()))),
+ "ms2", "ms3");
+ });
+ }
+
+ public void testManifestShortcuts_missingMandatoryFields() {
+ // Start with no apps installed.
+ uninstallPackage(USER_0, CALLING_PACKAGE_1);
+ uninstallPackage(USER_0, CALLING_PACKAGE_2);
+ uninstallPackage(USER_0, CALLING_PACKAGE_3);
+ uninstallPackage(USER_0, CALLING_PACKAGE_4);
+
+ mService.handleUnlockUser(USER_0);
+
+ // Make sure no manifest shortcuts.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ });
+
+ // Package 1 updated, which has one valid manifest shortcut and one invalid.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_error_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "x1");
+ });
+
+ // Package 1 updated, which has one valid manifest shortcut and one invalid.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_error_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "x2");
+ });
+
+ // Package 1 updated, which has one valid manifest shortcut and one invalid.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_error_3);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "x3");
+ });
+ }
+
+ public void testManifestShortcuts_intentDefinitions() {
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_error_4);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // Make sure invalid ones are not published.
+ // Note that at this point disabled ones don't show up because they weren't pinned.
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2")
+ .areAllManifest()
+ .areAllNotDynamic()
+ .areAllNotPinned()
+ .areAllImmutable()
+ .areAllEnabled()
+ .forShortcutWithId("ms1", si -> {
+ assertTrue(si.isEnabled());
+ assertEquals("action1", si.getIntent().getAction());
+ })
+ .forShortcutWithId("ms2", si -> {
+ assertTrue(si.isEnabled());
+ assertEquals("action2_1", si.getIntent().getAction());
+ });
+ });
+
+ // Publish 5 enabled to pin some, so we can later test disabled manfiest shortcuts..
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // Make sure 5 manifest shortcuts are published.
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "ms3", "ms4", "ms5")
+ .areAllManifest()
+ .areAllNotDynamic()
+ .areAllNotPinned()
+ .areAllImmutable()
+ .areAllEnabled();
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("ms3", "ms4", "ms5"), HANDLE_USER_0);
+ });
+
+ // Make sure they're pinned.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "ms3", "ms4", "ms5")
+ .selectByIds("ms1", "ms2")
+ .areAllNotPinned()
+ .areAllEnabled()
+
+ .revertToOriginalList()
+ .selectByIds("ms3", "ms4", "ms5")
+ .areAllPinned()
+ .areAllEnabled();
+ });
+
+ // Update the app.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_error_4);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Make sure 3, 4 and 5 still exist but disabled.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "ms3", "ms4", "ms5")
+ .areAllNotDynamic()
+ .areAllImmutable()
+
+ .selectByIds("ms1", "ms2")
+ .areAllManifest()
+ .areAllNotPinned()
+ .areAllEnabled()
+
+ .revertToOriginalList()
+ .selectByIds("ms3", "ms4", "ms5")
+ .areAllNotManifest()
+ .areAllPinned()
+ .areAllDisabled()
+
+ .revertToOriginalList()
+ .forShortcutWithId("ms1", si -> {
+ assertEquals(si.getId(), "action1", si.getIntent().getAction());
+ })
+ .forShortcutWithId("ms2", si -> {
+ assertEquals(si.getId(), "action2_1", si.getIntent().getAction());
+ })
+ .forShortcutWithId("ms3", si -> {
+ assertEquals(si.getId(), Intent.ACTION_VIEW, si.getIntent().getAction());
+ })
+ .forShortcutWithId("ms4", si -> {
+ assertEquals(si.getId(), Intent.ACTION_VIEW, si.getIntent().getAction());
+ })
+ .forShortcutWithId("ms5", si -> {
+ assertEquals(si.getId(), "action", si.getIntent().getAction());
+ });
+ });
+ }
+
+ public void testManifestShortcuts_checkAllFields() {
+ mService.handleUnlockUser(USER_0);
+
+ // Package 1 updated, which has one valid manifest shortcut and one invalid.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "ms3", "ms4", "ms5")
+ .areAllManifest()
+ .areAllImmutable()
+ .areAllEnabled()
+ .areAllNotPinned()
+ .areAllNotDynamic()
+
+ .forShortcutWithId("ms1", si -> {
+ assertEquals(R.drawable.icon1, si.getIconResourceId());
+ assertEquals(new ComponentName(CALLING_PACKAGE_1,
+ ShortcutActivity.class.getName()),
+ si.getActivity());
+
+ assertEquals(R.string.shortcut_title1, si.getTitleResId());
+ assertEquals("r" + R.string.shortcut_title1, si.getTitleResName());
+ assertEquals(R.string.shortcut_text1, si.getTextResId());
+ assertEquals("r" + R.string.shortcut_text1, si.getTextResName());
+ assertEquals(R.string.shortcut_disabled_message1,
+ si.getDisabledMessageResourceId());
+ assertEquals("r" + R.string.shortcut_disabled_message1,
+ si.getDisabledMessageResName());
+
+ assertEquals(set("android.shortcut.conversation", "android.shortcut.media"),
+ si.getCategories());
+ assertEquals("action1", si.getIntent().getAction());
+ assertEquals(Uri.parse("http://a.b.c/1"), si.getIntent().getData());
+ })
+
+ .forShortcutWithId("ms2", si -> {
+ assertEquals("ms2", si.getId());
+ assertEquals(R.drawable.icon2, si.getIconResourceId());
+
+ assertEquals(R.string.shortcut_title2, si.getTitleResId());
+ assertEquals("r" + R.string.shortcut_title2, si.getTitleResName());
+ assertEquals(R.string.shortcut_text2, si.getTextResId());
+ assertEquals("r" + R.string.shortcut_text2, si.getTextResName());
+ assertEquals(R.string.shortcut_disabled_message2,
+ si.getDisabledMessageResourceId());
+ assertEquals("r" + R.string.shortcut_disabled_message2,
+ si.getDisabledMessageResName());
+
+ assertEquals(set("android.shortcut.conversation"), si.getCategories());
+ assertEquals("action2", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getData());
+ })
+
+ .forShortcutWithId("ms3", si -> {
+ assertEquals(0, si.getIconResourceId());
+ assertEquals(R.string.shortcut_title1, si.getTitleResId());
+ assertEquals("r" + R.string.shortcut_title1, si.getTitleResName());
+
+ assertEquals(0, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(0, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+
+ assertEmpty(si.getCategories());
+ assertEquals("android.intent.action.VIEW", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getData());
+ })
+
+ .forShortcutWithId("ms4", si -> {
+ assertEquals(0, si.getIconResourceId());
+ assertEquals(R.string.shortcut_title2, si.getTitleResId());
+ assertEquals("r" + R.string.shortcut_title2, si.getTitleResName());
+
+ assertEquals(0, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(0, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+
+ assertEquals(set("cat"), si.getCategories());
+ assertEquals("android.intent.action.VIEW2", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getData());
+ })
+
+ .forShortcutWithId("ms5", si -> {
+ si = getCallerShortcut("ms5");
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("http://www/", si.getIntent().getData().toString());
+ assertEquals("foo/bar", si.getIntent().getType());
+ assertEquals(
+ new ComponentName("abc", ".xyz"), si.getIntent().getComponent());
+
+ assertEquals(set("cat1", "cat2"), si.getIntent().getCategories());
+ assertEquals("value1", si.getIntent().getStringExtra("key1"));
+ assertEquals("value2", si.getIntent().getStringExtra("key2"));
+ });
+ });
+ }
+
+ public void testManifestShortcuts_localeChange() {
+ mService.handleUnlockUser(USER_0);
+
+ // Package 1 updated, which has one valid manifest shortcut and one invalid.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.setDynamicShortcuts(list(makeShortcutWithTitle("s1", "title")));
+
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2");
+
+ // check first shortcut.
+ ShortcutInfo si = getCallerShortcut("ms1");
+
+ assertEquals("ms1", si.getId());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title1 + "/en",
+ si.getTitle());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text1 + "/en",
+ si.getText());
+ assertEquals("string-com.android.test.1-user:0-res:"
+ + R.string.shortcut_disabled_message1 + "/en",
+ si.getDisabledMessage());
+ assertEquals(START_TIME, si.getLastChangedTimestamp());
+
+ // check another
+ si = getCallerShortcut("ms2");
+
+ assertEquals("ms2", si.getId());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title2 + "/en",
+ si.getTitle());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text2 + "/en",
+ si.getText());
+ assertEquals("string-com.android.test.1-user:0-res:"
+ + R.string.shortcut_disabled_message2 + "/en",
+ si.getDisabledMessage());
+ assertEquals(START_TIME, si.getLastChangedTimestamp());
+
+ // Check the dynamic one.
+ si = getCallerShortcut("s1");
+
+ assertEquals("s1", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals(null, si.getText());
+ assertEquals(null, si.getDisabledMessage());
+ assertEquals(START_TIME, si.getLastChangedTimestamp());
+ });
+
+ mInjectedCurrentTimeMillis++;
+
+ mInjectedLocale = Locale.JAPANESE;
+ mInternal.onSystemLocaleChangedNoLock();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // check first shortcut.
+ ShortcutInfo si = getCallerShortcut("ms1");
+
+ assertEquals("ms1", si.getId());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title1 + "/ja",
+ si.getTitle());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text1 + "/ja",
+ si.getText());
+ assertEquals("string-com.android.test.1-user:0-res:"
+ + R.string.shortcut_disabled_message1 + "/ja",
+ si.getDisabledMessage());
+ assertEquals(START_TIME + 1, si.getLastChangedTimestamp());
+
+ // check another
+ si = getCallerShortcut("ms2");
+
+ assertEquals("ms2", si.getId());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title2 + "/ja",
+ si.getTitle());
+ assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text2 + "/ja",
+ si.getText());
+ assertEquals("string-com.android.test.1-user:0-res:"
+ + R.string.shortcut_disabled_message2 + "/ja",
+ si.getDisabledMessage());
+ assertEquals(START_TIME + 1, si.getLastChangedTimestamp());
+
+ // Check the dynamic one. (locale change shouldn't affect.)
+ si = getCallerShortcut("s1");
+
+ assertEquals("s1", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals(null, si.getText());
+ assertEquals(null, si.getDisabledMessage());
+ assertEquals(START_TIME, si.getLastChangedTimestamp()); // Not changed.
+ });
+ }
+
+ public void testManifestShortcuts_updateAndDisabled_notPinned() {
+ mService.handleUnlockUser(USER_0);
+
+ // First, just publish a manifest shortcut.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+
+ // Make sure there's no other dangling shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "ms1");
+ });
+
+ // Now version up, the manifest shortcut is disabled now.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1_disable);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Because shortcut 1 wasn't pinned, it'll just go away.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertEmpty(mManager.getPinnedShortcuts());
+
+ // Make sure there's no other dangling shortcuts.
+ assertEmpty(getCallerShortcuts());
+ });
+ }
+
+ public void testManifestShortcuts_updateAndDisabled_pinned() {
+ mService.handleUnlockUser(USER_0);
+
+ // First, just publish a manifest shortcut.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Only the valid one is published.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+ assertEmpty(mManager.getPinnedShortcuts());
+
+ // Make sure there's no other dangling shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "ms1");
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms1"), HANDLE_USER_0);
+ });
+
+ // Now upgrade, the manifest shortcut is disabled now.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1_disable);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ // Because shortcut 1 was pinned, it'll still exist as pinned, but disabled.
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEmpty(mManager.getManifestShortcuts());
+ assertShortcutIds(assertAllNotManifest(assertAllImmutable(assertAllDisabled(
+ mManager.getPinnedShortcuts()))),
+ "ms1");
+
+ // Make sure the fields are updated.
+ ShortcutInfo si = getCallerShortcut("ms1");
+
+ assertEquals("ms1", si.getId());
+ assertEquals(R.drawable.icon2, si.getIconResourceId());
+ assertEquals(R.string.shortcut_title2, si.getTitleResId());
+ assertEquals(R.string.shortcut_text2, si.getTextResId());
+ assertEquals(R.string.shortcut_disabled_message2, si.getDisabledMessageResourceId());
+ assertEquals(Intent.ACTION_VIEW, si.getIntent().getAction());
+
+ // Make sure there's no other dangling shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "ms1");
+ });
+ }
+
+ public void testManifestShortcuts_duplicateInSingleActivity() {
+ mService.handleUnlockUser(USER_0);
+
+ // The XML has two shortcuts with the same ID.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2_duplicate);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1");
+
+ // Make sure the first one has survived. (the second one has a different title.)
+ ShortcutInfo si = getCallerShortcut("ms1");
+ assertEquals(R.string.shortcut_title1, si.getTitleResId());
+
+ // Make sure there's no other dangling shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "ms1");
+ });
+ }
+
+ public void testManifestShortcuts_duplicateInTwoActivities() {
+ mService.handleUnlockUser(USER_0);
+
+ // ShortcutActivity has shortcut ms1
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+
+ // ShortcutActivity2 has two shortcuts, ms1 and ms2.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+
+ // ms1 should belong to ShortcutActivity.
+ ShortcutInfo si = getCallerShortcut("ms1");
+ assertEquals(R.string.shortcut_title1, si.getTitleResId());
+ assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ si.getActivity());
+ assertEquals(0, si.getRank());
+
+ // ms2 should belong to ShortcutActivity*2*.
+ si = getCallerShortcut("ms2");
+ assertEquals(R.string.shortcut_title2, si.getTitleResId());
+ assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
+ si.getActivity());
+
+ // Also check the ranks
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()))
+ .haveRanksInOrder("ms1");
+ assertWith(getCallerShortcuts()).selectManifest()
+ .selectByActivity(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()))
+ .haveRanksInOrder("ms2", "ms3", "ms4", "ms5");
+
+ // Make sure there's no other dangling shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "ms1", "ms2", "ms3", "ms4", "ms5");
+ });
+ }
+
+ /**
+ * Manifest shortcuts cannot override shortcuts that were published via the APIs.
+ */
+ public void testManifestShortcuts_cannotOverrideNonManifest() {
+ mService.handleUnlockUser(USER_0);
+
+ // Create a non-pinned dynamic shortcut and a non-dynamic pinned shortcut.
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.setDynamicShortcuts(list(
+ makeShortcut("ms1", "title1",
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ /* icon */ null, new Intent("action1"), /* rank */ 0),
+ makeShortcut("ms2", "title2",
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ /* icon */ null, new Intent("action1"), /* rank */ 0)));
+ });
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2"), HANDLE_USER_0);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeDynamicShortcuts(list("ms2"));
+
+ assertShortcutIds(mManager.getDynamicShortcuts(), "ms1");
+ assertShortcutIds(mManager.getPinnedShortcuts(), "ms2");
+ assertEmpty(mManager.getManifestShortcuts());
+ });
+
+ // Then update the app with 5 manifest shortcuts.
+ // Make sure "ms1" and "ms2" won't be replaced.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllNotManifest(mManager.getDynamicShortcuts()), "ms1");
+ assertShortcutIds(assertAllNotManifest(mManager.getPinnedShortcuts()), "ms2");
+ assertShortcutIds(assertAllManifest(mManager.getManifestShortcuts()),
+ "ms3", "ms4", "ms5");
+
+ // ms1 and ms2 shouold keep the original title.
+ ShortcutInfo si = getCallerShortcut("ms1");
+ assertEquals("title1", si.getTitle());
+
+ si = getCallerShortcut("ms2");
+ assertEquals("title2", si.getTitle());
+ });
+ }
+
+ protected void checkManifestShortcuts_immutable_verify() {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertShortcutIds(assertAllNotManifest(assertAllEnabled(
+ mManager.getDynamicShortcuts())),
+ "s1");
+ assertShortcutIds(assertAllManifest(assertAllEnabled(
+ mManager.getManifestShortcuts())),
+ "ms1");
+ assertShortcutIds(assertAllNotManifest(assertAllDisabled(
+ mManager.getPinnedShortcuts())),
+ "ms2");
+
+ assertEquals("t1", getCallerShortcut("s1").getTitle());
+
+ // Make sure there are no other shortcuts.
+ assertShortcutIds(getCallerShortcuts(), "s1", "ms1", "ms2");
+ });
+ }
+
+ /**
+ * Make sure the APIs won't work on manifest shortcuts.
+ */
+ public void testManifestShortcuts_immutable() {
+ mService.handleUnlockUser(USER_0);
+
+ // Create a non-pinned manifest shortcut, a pinned shortcut that was originally
+ // a manifest shortcut, as well as a dynamic shortcut.
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2"), HANDLE_USER_0);
+ });
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.addDynamicShortcuts(list(makeShortcutWithTitle("s1", "t1")));
+ });
+
+ checkManifestShortcuts_immutable_verify();
+
+ // Note that even though the first argument is not immutable and only the second one
+ // is immutable, the first argument should not be executed either.
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.setDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms1")));
+ });
+ assertCannotUpdateImmutable(() -> {
+ mManager.setDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms2")));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.addDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms1")));
+ });
+ assertCannotUpdateImmutable(() -> {
+ mManager.addDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms2")));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.updateShortcuts(list(makeShortcut("s1"), makeShortcut("ms1")));
+ });
+ assertCannotUpdateImmutable(() -> {
+ mManager.updateShortcuts(list(makeShortcut("s1"), makeShortcut("ms2")));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.removeDynamicShortcuts(list("s1", "ms1"));
+ });
+ assertCannotUpdateImmutable(() -> {
+ mManager.removeDynamicShortcuts(list("s2", "ms2"));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.disableShortcuts(list("s1", "ms1"));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertCannotUpdateImmutable(() -> {
+ mManager.enableShortcuts(list("s1", "ms2"));
+ });
+ });
+ checkManifestShortcuts_immutable_verify();
+ }
+
+
+ /**
+ * Make sure the APIs won't work on manifest shortcuts.
+ */
+ public void testManifestShortcuts_tooMany() {
+ // Change the max number of shortcuts.
+ mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
+
+ mService.handleUnlockUser(USER_0);
+
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_5);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // Only the first 3 should be published.
+ assertShortcutIds(mManager.getManifestShortcuts(), "ms1", "ms2", "ms3");
+ });
+ }
+
+ public void testMaxShortcutCount_set() {
+ // Change the max number of shortcuts.
+ mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
+ final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
+ final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
+ final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
+ final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
+ final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
+ final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1);
+ final ShortcutInfo s1_6 = makeShortcutWithActivity("s16", a1);
+ final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
+ final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
+ final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
+ final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
+
+ // 3 shortcuts for 2 activities -> okay
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ mManager.removeAllDynamicShortcuts();
+
+ // 4 shortcut for activity 1 -> too many.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3));
+ });
+ assertEmpty(mManager.getDynamicShortcuts());
+
+ // 4 shortcut for activity 2 -> too many.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4));
+ });
+ assertEmpty(mManager.getDynamicShortcuts());
+
+ // First, set 3. Then set 4, which should be ignored.
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13");
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.setDynamicShortcuts(list(s2_1, s2_2, s2_3, s2_4));
+ });
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13");
+
+ // Set will remove the old dynamic set, unlike add, so the following should pass.
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13");
+ mManager.setDynamicShortcuts(list(s1_4, s1_5, s1_6));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s14", "s15", "s16");
+
+ // Now, test with 2 manifest shortcuts.
+ mManager.removeAllDynamicShortcuts();
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ assertEquals(2, mManager.getManifestShortcuts().size());
+
+ // Setting 1 to activity 1 will work.
+ mManager.setDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s21", "s22", "s23");
+ assertEquals(2, mManager.getManifestShortcuts().size());
+
+ // But setting 2 will not.
+ mManager.removeAllDynamicShortcuts();
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s2_1, s2_2, s2_3));
+ });
+ assertEmpty(mManager.getDynamicShortcuts());
+ assertEquals(2, mManager.getManifestShortcuts().size());
+ });
+ }
+
+ public void testMaxShortcutCount_add() {
+ // Change the max number of shortcuts.
+ mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
+ final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
+ final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
+ final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
+ final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
+ final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
+ final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
+ final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
+ final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
+ final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
+
+ // 3 shortcuts for 2 activities -> okay
+ mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ mManager.removeAllDynamicShortcuts();
+
+ // 4 shortcut for activity 1 -> too many.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3));
+ });
+ assertEmpty(mManager.getDynamicShortcuts());
+
+ // 4 shortcut for activity 2 -> too many.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4));
+ });
+ assertEmpty(mManager.getDynamicShortcuts());
+
+ // First, set 3. Then add 1 more, which should be ignored.
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13");
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.addDynamicShortcuts(list(s1_4, s2_1));
+ });
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13");
+
+ // Update existing one, which should work.
+ mManager.addDynamicShortcuts(list(makeShortcutWithActivityAndTitle(
+ "s11", a1, "xxx"), s2_1));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21");
+ assertEquals("xxx", getCallerShortcut("s11").getTitle());
+
+ // Make sure pinned shortcuts won't affect.
+ // - Pin s11 - s13, and remove all dynamic.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"),
+ HANDLE_USER_0);
+ });
+ mManager.removeAllDynamicShortcuts();
+
+ assertEmpty(mManager.getDynamicShortcuts());
+ assertShortcutIds(mManager.getPinnedShortcuts(),
+ "s11", "s12", "s13");
+
+ // Then add dynamic.
+ mManager.addDynamicShortcuts(list(s1_4, s2_1, s2_2, s2_3));
+
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s14", "s21", "s22", "s23");
+ assertShortcutIds(mManager.getPinnedShortcuts(),
+ "s11", "s12", "s13");
+
+ // Adding "s11" and "s12" back, should work
+ mManager.addDynamicShortcuts(list(s1_1, s1_2));
+
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s14", "s11", "s12", "s21", "s22", "s23");
+ assertShortcutIds(mManager.getPinnedShortcuts(),
+ "s11", "s12", "s13");
+
+ // Adding back s13 doesn't work.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.addDynamicShortcuts(list(s1_3));
+ });
+
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
+ "s11", "s12", "s14");
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
+ "s21", "s22", "s23");
+
+ // Now swap the activities.
+ mManager.updateShortcuts(list(
+ makeShortcutWithActivity("s11", a2),
+ makeShortcutWithActivity("s21", a1)));
+
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
+ "s21", "s12", "s14");
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
+ "s11", "s22", "s23");
+
+ // Now, test with 2 manifest shortcuts.
+ mManager.removeAllDynamicShortcuts();
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ assertEquals(2, mManager.getManifestShortcuts().size());
+
+ // Adding one shortcut to activity 1 works fine.
+ mManager.addDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s21", "s22", "s23");
+ assertEquals(2, mManager.getManifestShortcuts().size());
+
+ // But adding one more doesn't.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.addDynamicShortcuts(list(s1_4, s2_1));
+ });
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s21", "s22", "s23");
+ assertEquals(2, mManager.getManifestShortcuts().size());
+ });
+ }
+
+ public void testMaxShortcutCount_update() {
+ // Change the max number of shortcuts.
+ mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
+ final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
+ final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
+ final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
+ final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
+ final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
+ final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1);
+ final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
+ final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
+ final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
+ final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
+
+ // 3 shortcuts for 2 activities -> okay
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ // Trying to move s11 from a1 to a2 should fail.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.updateShortcuts(list(makeShortcutWithActivity("s11", a2)));
+ });
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ // Trying to move s21 from a2 to a1 should also fail.
+ assertDynamicShortcutCountExceeded(() -> {
+ mManager.updateShortcuts(list(makeShortcutWithActivity("s21", a1)));
+ });
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ // But, if we do these two at the same time, it should work.
+ mManager.updateShortcuts(list(
+ makeShortcutWithActivity("s11", a2),
+ makeShortcutWithActivity("s21", a1)));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
+ "s21", "s12", "s13");
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
+ "s11", "s22", "s23");
+
+ // Then reset.
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s11", "s12", "s13", "s21", "s22", "s23");
+
+ // Pin some to have more shortcuts for a1.
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"),
+ HANDLE_USER_0);
+ });
+ mManager.setDynamicShortcuts(list(s1_4, s1_5, s2_1, s2_2, s2_3));
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s14", "s15", "s21", "s22", "s23");
+ assertShortcutIds(mManager.getPinnedShortcuts(),
+ "s11", "s12", "s13");
+
+ // a1 already has 2 dynamic shortcuts (and 3 pinned shortcuts that used to belong on it)
+ // But that doesn't matter for update -- the following should still work.
+ mManager.updateShortcuts(list(
+ makeShortcutWithActivityAndTitle("s11", a1, "xxx1"),
+ makeShortcutWithActivityAndTitle("s12", a1, "xxx2"),
+ makeShortcutWithActivityAndTitle("s13", a1, "xxx3"),
+ makeShortcutWithActivityAndTitle("s14", a1, "xxx4"),
+ makeShortcutWithActivityAndTitle("s15", a1, "xxx5")));
+ // All the shortcuts should still exist they all belong on same activities,
+ // with the updated titles.
+ assertShortcutIds(mManager.getDynamicShortcuts(),
+ "s14", "s15", "s21", "s22", "s23");
+ assertShortcutIds(mManager.getPinnedShortcuts(),
+ "s11", "s12", "s13");
+
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
+ "s14", "s15");
+ assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
+ "s21", "s22", "s23");
+
+ assertEquals("xxx1", getCallerShortcut("s11").getTitle());
+ assertEquals("xxx2", getCallerShortcut("s12").getTitle());
+ assertEquals("xxx3", getCallerShortcut("s13").getTitle());
+ assertEquals("xxx4", getCallerShortcut("s14").getTitle());
+ assertEquals("xxx5", getCallerShortcut("s15").getTitle());
+ });
+ }
+
+ public void testShortcutsPushedOutByManifest() {
+ // Change the max number of shortcuts.
+ mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
+ final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
+ final ShortcutInfo s1_1 = makeShortcutWithActivityAndRank("s11", a1, 4);
+ final ShortcutInfo s1_2 = makeShortcutWithActivityAndRank("s12", a1, 3);
+ final ShortcutInfo s1_3 = makeShortcutWithActivityAndRank("s13", a1, 2);
+ final ShortcutInfo s1_4 = makeShortcutWithActivityAndRank("s14", a1, 1);
+ final ShortcutInfo s1_5 = makeShortcutWithActivityAndRank("s15", a1, 0);
+ final ShortcutInfo s2_1 = makeShortcutWithActivityAndRank("s21", a2, 0);
+ final ShortcutInfo s2_2 = makeShortcutWithActivityAndRank("s22", a2, 1);
+ final ShortcutInfo s2_3 = makeShortcutWithActivityAndRank("s23", a2, 2);
+ final ShortcutInfo s2_4 = makeShortcutWithActivityAndRank("s24", a2, 3);
+ final ShortcutInfo s2_5 = makeShortcutWithActivityAndRank("s25", a2, 4);
+
+ // Initial state.
+ mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s21", "s22"),
+ HANDLE_USER_0);
+ });
+ mManager.setDynamicShortcuts(list(s1_2, s1_3, s1_4, s2_2, s2_3, s2_4));
+ assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
+ "s12", "s13", "s14",
+ "s22", "s23", "s24");
+ assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
+ "s11", "s12",
+ "s21", "s22");
+
+ // Add 1 manifest shortcut to a1.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ assertEquals(1, mManager.getManifestShortcuts().size());
+
+ // s12 removed.
+ assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
+ "s13", "s14",
+ "s22", "s23", "s24");
+ assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
+ "s11", "s12",
+ "s21", "s22");
+
+ // Add more manifest shortcuts.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_1_alt);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ assertEquals(3, mManager.getManifestShortcuts().size());
+
+ // Note the ones with the highest rank values (== least important) will be removed.
+ assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
+ "s14",
+ "s22", "s23");
+ assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
+ "s11", "s12",
+ "s21", "s22");
+
+ // Add more manifest shortcuts.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_5_alt); // manifest has 5, but max is 3, so a2 will have 3.
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ assertEquals(5, mManager.getManifestShortcuts().size());
+
+ assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
+ "s14" // a1 has 1 dynamic
+ ); // a2 has no dynamic
+ assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
+ "s11", "s12",
+ "s21", "s22");
+
+ // Update, no manifest shortucts. This doesn't affect anything.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_0);
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
+ R.xml.shortcut_0);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ assertEquals(0, mManager.getManifestShortcuts().size());
+
+ assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
+ "s14");
+ assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
+ "s11", "s12",
+ "s21", "s22");
+ });
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
new file mode 100644
index 0000000..fdd8f142
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -0,0 +1,1695 @@
+/*
+ * 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 static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.parceled;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.Manifest.permission;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Resources;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.frameworks.servicestests.R;
+
+/**
+ * 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.ShortcutManagerTest2 \
+ -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ */
+@SmallTest
+public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
+ // ShortcutInfo tests
+
+ public void testShortcutInfoMissingMandatoryFields() {
+ // Disable throttling.
+ mService.updateConfigurationLocked(
+ ShortcutService.ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999,"
+ + ShortcutService.ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
+ );
+
+ assertExpectException(
+ IllegalArgumentException.class,
+ "ID must be provided",
+ () -> new ShortcutInfo.Builder(getTestContext()).build());
+
+ assertExpectException(NullPointerException.class, "Intent action must be set",
+ () -> new ShortcutInfo.Builder(getTestContext()).setIntent(new Intent()));
+
+ assertExpectException(NullPointerException.class, "Activity must be provided", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()).setId("id").build();
+ assertTrue(getManager().setDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(
+ IllegalArgumentException.class, "Short label must be provided", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
+ .build();
+ assertTrue(getManager().setDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(
+ IllegalArgumentException.class, "Short label must be provided", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
+ .build();
+ assertTrue(getManager().addDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
+ .setShortLabel("x")
+ .build();
+ assertTrue(getManager().setDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
+ .setShortLabel("x")
+ .build();
+ assertTrue(getManager().addDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(
+ IllegalStateException.class, "package name mismatch", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("xxx", "s"))
+ .build();
+ assertTrue(getManager().setDynamicShortcuts(list(si)));
+ });
+
+ assertExpectException(
+ IllegalStateException.class, "package name mismatch", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("xxx", "s"))
+ .build();
+ assertTrue(getManager().addDynamicShortcuts(list(si)));
+ });
+ }
+
+ public void testShortcutInfoParcel() {
+ setCaller(CALLING_PACKAGE_1, USER_10);
+ ShortcutInfo si = parceled(new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setTitle("title")
+ .setIntent(makeIntent("action", ShortcutActivity.class))
+ .build());
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals(USER_10, si.getUserId());
+ assertEquals(HANDLE_USER_10, si.getUserHandle());
+ assertEquals("id", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals("action", si.getIntent().getAction());
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+
+ si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitle("title")
+ .setText("text")
+ .setDisabledMessage("dismes")
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ si.addFlags(ShortcutInfo.FLAG_PINNED);
+ si.setBitmapPath("abc");
+ si.setIconResourceId(456);
+
+ si = parceled(si);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(123, si.getIcon().getResId());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals("abc", si.getBitmapPath());
+ assertEquals(456, si.getIconResourceId());
+
+ assertEquals(0, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+ assertEquals(0, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(0, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+ }
+
+ public void testShortcutInfoParcel_resId() {
+ setCaller(CALLING_PACKAGE_1, USER_10);
+ ShortcutInfo si;
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+
+ si = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ si.addFlags(ShortcutInfo.FLAG_PINNED);
+ si.setBitmapPath("abc");
+ si.setIconResourceId(456);
+
+ lookupAndFillInResourceNames(si);
+
+ si = parceled(si);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(123, si.getIcon().getResId());
+ assertEquals(10, si.getTitleResId());
+ assertEquals("r10", si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals("r11", si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals("r12", si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals("abc", si.getBitmapPath());
+ assertEquals(456, si.getIconResourceId());
+ assertEquals("string/r456", si.getIconResName());
+ }
+
+ public void testShortcutInfoClone() {
+ setCaller(CALLING_PACKAGE_1, USER_11);
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitle("title")
+ .setText("text")
+ .setDisabledMessage("dismes")
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ sorig.addFlags(ShortcutInfo.FLAG_PINNED);
+ sorig.setBitmapPath("abc");
+ sorig.setIconResourceId(456);
+
+ lookupAndFillInResourceNames(sorig);
+
+ ShortcutInfo si = sorig.clone(/* clone flags*/ 0);
+
+ assertEquals(USER_11, si.getUserId());
+ assertEquals(HANDLE_USER_11, si.getUserHandle());
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(123, si.getIcon().getResId());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals("abc", si.getBitmapPath());
+ assertEquals(456, si.getIconResourceId());
+ assertEquals("string/r456", si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals(null, si.getIntent());
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals(null, si.getTitle());
+ assertEquals(null, si.getText());
+ assertEquals(null, si.getDisabledMessage());
+ assertEquals(null, si.getCategories());
+ assertEquals(null, si.getIntent());
+ assertEquals(0, si.getRank());
+ assertEquals(null, si.getExtras());
+
+ assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_KEY_FIELDS_ONLY, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+ }
+
+ public void testShortcutInfoClone_resId() {
+ setCaller(CALLING_PACKAGE_1, USER_11);
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ sorig.addFlags(ShortcutInfo.FLAG_PINNED);
+ sorig.setBitmapPath("abc");
+ sorig.setIconResourceId(456);
+
+ lookupAndFillInResourceNames(sorig);
+
+ ShortcutInfo si = sorig.clone(/* clone flags*/ 0);
+
+ assertEquals(USER_11, si.getUserId());
+ assertEquals(HANDLE_USER_11, si.getUserHandle());
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(123, si.getIcon().getResId());
+ assertEquals(10, si.getTitleResId());
+ assertEquals("r10", si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals("r11", si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals("r12", si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals("abc", si.getBitmapPath());
+ assertEquals(456, si.getIconResourceId());
+ assertEquals("string/r456", si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals(10, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals(10, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals(null, si.getIntent());
+ assertEquals(123, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
+
+ assertEquals(mClientContext.getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(new ComponentName("a", "b"), si.getActivity());
+ assertEquals(null, si.getIcon());
+ assertEquals(0, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+ assertEquals(0, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+ assertEquals(0, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+ assertEquals(null, si.getCategories());
+ assertEquals(null, si.getIntent());
+ assertEquals(0, si.getRank());
+ assertEquals(null, si.getExtras());
+
+ assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_KEY_FIELDS_ONLY, si.getFlags());
+ assertEquals(null, si.getBitmapPath());
+
+ assertEquals(456, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+ }
+
+ public void testShortcutInfoClone_minimum() {
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setTitle("title")
+ .setIntent(makeIntent("action", ShortcutActivity.class))
+ .build();
+ ShortcutInfo si = sorig.clone(/* clone flags*/ 0);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals(null, si.getCategories());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals(null, si.getCategories());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals("title", si.getTitle());
+ assertEquals(null, si.getIntent());
+ assertEquals(null, si.getCategories());
+
+ si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
+
+ assertEquals(getTestContext().getPackageName(), si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(null, si.getTitle());
+ assertEquals(null, si.getIntent());
+ assertEquals(null, si.getCategories());
+ }
+
+ public void testShortcutInfoCopyNonNullFieldsFrom() throws InterruptedException {
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitle("title")
+ .setText("text")
+ .setDisabledMessage("dismes")
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ sorig.addFlags(ShortcutInfo.FLAG_PINNED);
+ sorig.setBitmapPath("abc");
+ sorig.setIconResourceId(456);
+
+ lookupAndFillInResourceNames(sorig);
+
+ ShortcutInfo si;
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setActivity(new ComponentName("x", "y")).build());
+ assertEquals("text", si.getText());
+ assertEquals(123, si.getRank());
+ assertEquals(new ComponentName("x", "y"), si.getActivity());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIcon(Icon.createWithResource(mClientContext, 456)).build());
+ assertEquals("text", si.getText());
+ assertEquals(456, si.getIcon().getResId());
+ assertEquals(0, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+ assertEquals(null, si.getBitmapPath());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTitle("xyz").build());
+ assertEquals("text", si.getText());
+ assertEquals("xyz", si.getTitle());
+ assertEquals(0, si.getTitleResId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTitleResId(123).build());
+ assertEquals("text", si.getText());
+ assertEquals(null, si.getTitle());
+ assertEquals(123, si.getTitleResId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setText("xxx").build());
+ assertEquals(123, si.getRank());
+ assertEquals("xxx", si.getText());
+ assertEquals(0, si.getTextResId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTextResId(1111).build());
+ assertEquals(123, si.getRank());
+ assertEquals(null, si.getText());
+ assertEquals(1111, si.getTextResId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setDisabledMessage("xxx").build());
+ assertEquals(123, si.getRank());
+ assertEquals("xxx", si.getDisabledMessage());
+ assertEquals(0, si.getDisabledMessageResourceId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setDisabledMessageResId(11111).build());
+ assertEquals(123, si.getRank());
+ assertEquals(null, si.getDisabledMessage());
+ assertEquals(11111, si.getDisabledMessageResourceId());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setCategories(set()).build());
+ assertEquals("text", si.getText());
+ assertEquals(set(), si.getCategories());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setCategories(set("x")).build());
+ assertEquals("text", si.getText());
+ assertEquals(set("x"), si.getCategories());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIntent(makeIntent("action2", ShortcutActivity.class)).build());
+ assertEquals("text", si.getText());
+ assertEquals("action2", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getStringExtra("key"));
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIntent(makeIntent("action3", ShortcutActivity.class, "key", "x")).build());
+ assertEquals("text", si.getText());
+ assertEquals("action3", si.getIntent().getAction());
+ assertEquals("x", si.getIntent().getStringExtra("key"));
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setRank(999).build());
+ assertEquals("text", si.getText());
+ assertEquals(999, si.getRank());
+
+
+ PersistableBundle pb2 = new PersistableBundle();
+ pb2.putInt("x", 99);
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setExtras(pb2).build());
+ assertEquals("text", si.getText());
+ assertEquals(99, si.getExtras().getInt("x"));
+ }
+
+ public void testShortcutInfoCopyNonNullFieldsFrom_resId() throws InterruptedException {
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext())
+ .setId("id")
+ .setActivity(new ComponentName("a", "b"))
+ .setIcon(Icon.createWithResource(mClientContext, 123))
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+ sorig.addFlags(ShortcutInfo.FLAG_PINNED);
+ sorig.setBitmapPath("abc");
+ sorig.setIconResourceId(456);
+
+ ShortcutInfo si;
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setActivity(new ComponentName("x", "y")).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(new ComponentName("x", "y"), si.getActivity());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIcon(Icon.createWithResource(mClientContext, 456)).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(456, si.getIcon().getResId());
+ assertEquals(0, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+ assertEquals(null, si.getBitmapPath());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTitle("xyz").build());
+ assertEquals(11, si.getTextResId());
+ assertEquals("xyz", si.getTitle());
+ assertEquals(0, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTitleResId(123).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(null, si.getTitle());
+ assertEquals(123, si.getTitleResId());
+ assertEquals(null, si.getTitleResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setText("xxx").build());
+ assertEquals(123, si.getRank());
+ assertEquals("xxx", si.getText());
+ assertEquals(0, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setTextResId(1111).build());
+ assertEquals(123, si.getRank());
+ assertEquals(null, si.getText());
+ assertEquals(1111, si.getTextResId());
+ assertEquals(null, si.getTextResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setDisabledMessage("xxx").build());
+ assertEquals(123, si.getRank());
+ assertEquals("xxx", si.getDisabledMessage());
+ assertEquals(0, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setDisabledMessageResId(11111).build());
+ assertEquals(123, si.getRank());
+ assertEquals(null, si.getDisabledMessage());
+ assertEquals(11111, si.getDisabledMessageResourceId());
+ assertEquals(null, si.getDisabledMessageResName());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setCategories(set()).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(set(), si.getCategories());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setCategories(set("x")).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(set("x"), si.getCategories());
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIntent(makeIntent("action2", ShortcutActivity.class)).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals("action2", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getStringExtra("key"));
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setIntent(makeIntent("action3", ShortcutActivity.class, "key", "x")).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals("action3", si.getIntent().getAction());
+ assertEquals("x", si.getIntent().getStringExtra("key"));
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setRank(999).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(999, si.getRank());
+
+
+ PersistableBundle pb2 = new PersistableBundle();
+ pb2.putInt("x", 99);
+
+ si = sorig.clone(/* flags=*/ 0);
+ si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
+ .setExtras(pb2).build());
+ assertEquals(11, si.getTextResId());
+ assertEquals(99, si.getExtras().getInt("x"));
+ }
+
+ public void testShortcutInfoSaveAndLoad() throws InterruptedException {
+ setCaller(CALLING_PACKAGE_1, USER_10);
+
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIcon(bmp32x32)
+ .setTitle("title")
+ .setText("text")
+ .setDisabledMessage("dismes")
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+
+ ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext)
+ .setId("id2")
+ .setTitle("x")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(456)
+ .build();
+
+ mManager.addDynamicShortcuts(list(sorig, sorig2));
+
+ Thread.sleep(2);
+ final long now = System.currentTimeMillis();
+
+ // Save and load.
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_10);
+
+ ShortcutInfo si;
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_10);
+
+ assertEquals(USER_10, si.getUserId());
+ assertEquals(HANDLE_USER_10, si.getUserHandle());
+ assertEquals(CALLING_PACKAGE_1, si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
+ assertEquals(null, si.getIcon());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(0, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE
+ | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
+ assertNotNull(si.getBitmapPath()); // Something should be set.
+ assertEquals(0, si.getIconResourceId());
+ assertTrue(si.getLastChangedTimestamp() < now);
+
+ // Make sure ranks are saved too. Because of the auto-adjusting, we need two shortcuts
+ // to test it.
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_10);
+ assertEquals(1, si.getRank());
+ }
+
+ public void testShortcutInfoSaveAndLoad_resId() throws InterruptedException {
+ setCaller(CALLING_PACKAGE_1, USER_10);
+
+ final Icon res32x32 = Icon.createWithResource(mClientContext, R.drawable.black_32x32);
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIcon(res32x32)
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+
+ ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext)
+ .setId("id2")
+ .setTitle("x")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(456)
+ .build();
+
+ mManager.addDynamicShortcuts(list(sorig, sorig2));
+
+ Thread.sleep(2);
+ final long now = System.currentTimeMillis();
+
+ // Save and load.
+ mService.saveDirtyInfo();
+ initService();
+ mService.handleUnlockUser(USER_10);
+
+ ShortcutInfo si;
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_10);
+
+ assertEquals(USER_10, si.getUserId());
+ assertEquals(HANDLE_USER_10, si.getUserHandle());
+ assertEquals(CALLING_PACKAGE_1, si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
+ assertEquals(null, si.getIcon());
+ assertEquals(10, si.getTitleResId());
+ assertEquals("r10", si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals("r11", si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals("r12", si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(0, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_RES
+ | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
+ assertNull(si.getBitmapPath());
+ assertEquals(R.drawable.black_32x32, si.getIconResourceId());
+ assertTrue(si.getLastChangedTimestamp() < now);
+
+ // Make sure ranks are saved too. Because of the auto-adjusting, we need two shortcuts
+ // to test it.
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_10);
+ assertEquals(1, si.getRank());
+ }
+
+ public void testShortcutInfoSaveAndLoad_forBackup() {
+ setCaller(CALLING_PACKAGE_1, USER_0);
+
+ final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+ getTestContext().getResources(), R.drawable.black_32x32));
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIcon(bmp32x32)
+ .setTitle("title")
+ .setText("text")
+ .setDisabledMessage("dismes")
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+
+ ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext)
+ .setId("id2")
+ .setTitle("x")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(456)
+ .build();
+
+ mManager.addDynamicShortcuts(list(sorig, sorig2));
+
+ // Dynamic shortcuts won't be backed up, so we need to pin it.
+ setCaller(LAUNCHER_1, USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id", "id2"), HANDLE_USER_0);
+
+ // Do backup & restore.
+ backupAndRestore();
+
+ mService.handleUnlockUser(USER_0); // Load user-0.
+
+ ShortcutInfo si;
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_0);
+
+ assertEquals(CALLING_PACKAGE_1, si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
+ assertEquals(null, si.getIcon());
+ assertEquals("title", si.getTitle());
+ assertEquals("text", si.getText());
+ assertEquals("dismes", si.getDisabledMessage());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(0, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
+ assertNull(si.getBitmapPath()); // No icon.
+ assertEquals(0, si.getIconResourceId());
+
+ // Note when restored from backup, it's no longer dynamic, so shouldn't have a rank.
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_0);
+ assertEquals(0, si.getRank());
+ }
+
+ public void testShortcutInfoSaveAndLoad_forBackup_resId() {
+ setCaller(CALLING_PACKAGE_1, USER_0);
+
+ final Icon res32x32 = Icon.createWithResource(mClientContext, R.drawable.black_32x32);
+
+ PersistableBundle pb = new PersistableBundle();
+ pb.putInt("k", 1);
+ ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+ .setId("id")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIcon(res32x32)
+ .setTitleResId(10)
+ .setTextResId(11)
+ .setDisabledMessageResId(12)
+ .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(123)
+ .setExtras(pb)
+ .build();
+
+ ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext)
+ .setId("id2")
+ .setTitle("x")
+ .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+ .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+ .setRank(456)
+ .build();
+
+ mManager.addDynamicShortcuts(list(sorig, sorig2));
+
+ // Dynamic shortcuts won't be backed up, so we need to pin it.
+ setCaller(LAUNCHER_1, USER_0);
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id", "id2"), HANDLE_USER_0);
+
+ // Do backup & restore.
+ backupAndRestore();
+
+ mService.handleUnlockUser(USER_0); // Load user-0.
+
+ ShortcutInfo si;
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_0);
+
+ assertEquals(CALLING_PACKAGE_1, si.getPackage());
+ assertEquals("id", si.getId());
+ assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
+ assertEquals(null, si.getIcon());
+ assertEquals(10, si.getTitleResId());
+ assertEquals("r10", si.getTitleResName());
+ assertEquals(11, si.getTextResId());
+ assertEquals("r11", si.getTextResName());
+ assertEquals(12, si.getDisabledMessageResourceId());
+ assertEquals("r12", si.getDisabledMessageResName());
+ assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+ assertEquals("action", si.getIntent().getAction());
+ assertEquals("val", si.getIntent().getStringExtra("key"));
+ assertEquals(0, si.getRank());
+ assertEquals(1, si.getExtras().getInt("k"));
+
+ assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
+ assertNull(si.getBitmapPath()); // No icon.
+ assertEquals(0, si.getIconResourceId());
+ assertEquals(null, si.getIconResName());
+
+ // Note when restored from backup, it's no longer dynamic, so shouldn't have a rank.
+ si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_0);
+ assertEquals(0, si.getRank());
+ }
+
+ public void testThrottling() {
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(1, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ // Reached the max
+
+ mInjectedCurrentTimeMillis++;
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ // Still throttled
+ mInjectedCurrentTimeMillis = START_TIME + INTERVAL - 1;
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ // Now it should work.
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1))); // fail
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 2, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(1, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 2, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 2, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 2, mManager.getRateLimitResetTime());
+
+ // 4 hours later...
+ mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 5, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(1, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 5, mManager.getRateLimitResetTime());
+
+ // Make sure getRemainingCallCount() itself gets reset without calling setDynamicShortcuts().
+ mInjectedCurrentTimeMillis = START_TIME + 8 * INTERVAL;
+ assertEquals(3, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 9, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL * 9, mManager.getRateLimitResetTime());
+ }
+
+ public void testThrottling_rewind() {
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ mInjectedCurrentTimeMillis = 12345; // Clock reset!
+
+ // Since the clock looks invalid, the counter shouldn't have reset.
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ // Forward again. Still haven't reset yet.
+ mInjectedCurrentTimeMillis = START_TIME + INTERVAL - 1;
+ assertEquals(2, mManager.getRemainingCallCount());
+ assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
+
+ // Now rewind -- this will reset the counters.
+ mInjectedCurrentTimeMillis = START_TIME - 100000;
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ // Forward again, should be reset now.
+ mInjectedCurrentTimeMillis += INTERVAL;
+ assertEquals(3, mManager.getRemainingCallCount());
+ }
+
+ public void testThrottling_perPackage() {
+ final ShortcutInfo si1 = makeShortcut("shortcut1");
+
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(1, mManager.getRemainingCallCount());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+
+ // Reached the max
+
+ mInjectedCurrentTimeMillis++;
+ assertFalse(mManager.setDynamicShortcuts(list(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(list(si2)));
+ assertEquals(2, mManager.getRemainingCallCount());
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si2)));
+ assertEquals(1, mManager.getRemainingCallCount());
+
+ // Back to the original caller, still throttled.
+ mInjectedClientPackage = CALLING_PACKAGE_1;
+ mInjectedCallingUid = CALLING_UID_1;
+
+ mInjectedCurrentTimeMillis = START_TIME + INTERVAL - 1;
+ assertEquals(0, mManager.getRemainingCallCount());
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+ assertEquals(0, mManager.getRemainingCallCount());
+
+ // Now it should work.
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+
+ mInjectedCurrentTimeMillis++;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+
+ mInjectedCurrentTimeMillis++;
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+
+ mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL;
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ assertFalse(mManager.setDynamicShortcuts(list(si1)));
+
+ mInjectedClientPackage = CALLING_PACKAGE_2;
+ mInjectedCallingUid = CALLING_UID_2;
+
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ assertTrue(mManager.setDynamicShortcuts(list(si2)));
+ assertTrue(mManager.setDynamicShortcuts(list(si2)));
+ assertTrue(mManager.setDynamicShortcuts(list(si2)));
+ assertFalse(mManager.setDynamicShortcuts(list(si2)));
+ }
+
+ public void testThrottling_localeChanges() {
+ prepareCrossProfileDataSet();
+
+ dumpsysOnLogcat("Before save & load");
+
+ mService.saveDirtyInfo();
+ initService();
+
+ final long origSequenceNumber = mService.getLocaleChangeSequenceNumber();
+
+ mInternal.onSystemLocaleChangedNoLock();
+ assertEquals(origSequenceNumber + 1, mService.getLocaleChangeSequenceNumber());
+
+ // Note at this point only user-0 is loaded, and the counters are reset for this user,
+ // but it will work for other users too, because we persist when
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+
+ mService.saveDirtyInfo();
+ initService();
+
+ // Make sure the counter is persisted.
+ assertEquals(origSequenceNumber + 1, mService.getLocaleChangeSequenceNumber());
+ }
+
+ public void testThrottling_foreground() throws Exception {
+ prepareCrossProfileDataSet();
+
+ dumpsysOnLogcat("Before save & load");
+
+ mService.saveDirtyInfo();
+ initService();
+
+ // We need to update the current time from time to time, since some of the internal checks
+ // rely on the time being correctly incremented.
+ mInjectedCurrentTimeMillis++;
+
+ // First, all packages have less than 3 (== initial value) remaining calls.
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mInjectedCurrentTimeMillis++;
+
+ // State changed, but not foreground, so no resetting.
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_1, ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mInjectedCurrentTimeMillis++;
+
+ // State changed, package1 foreground, reset.
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_1, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_1, ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+
+ mInjectedCurrentTimeMillis++;
+
+ // Different app comes to foreground briefly, and goes back to background.
+ // Now, make sure package 2's counter is reset, even in this case.
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_2, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_2, ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mInjectedCurrentTimeMillis++;
+
+ // Do the same thing one more time. This would catch the bug with mixuing up
+ // the current time and the elapsed time.
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ mManager.updateShortcuts(list(makeShortcut("s")));
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_2, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ mService.mUidObserver.onUidStateChanged(
+ CALLING_UID_2, ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mInjectedCurrentTimeMillis++;
+
+ // Package 1 on user-10 comes to foreground.
+ // Now, also try calling some APIs and make sure foreground apps don't get throttled.
+ mService.mUidObserver.onUidStateChanged(
+ UserHandle.getUid(USER_10, CALLING_UID_1),
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(0, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(0, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(0, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(0, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(0, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+ mManager.setDynamicShortcuts(list(makeShortcut("s")));
+
+ assertEquals(3, mManager.getRemainingCallCount()); // Still 3!
+ });
+ }
+
+
+ public void testThrottling_resetByInternalCall() throws Exception {
+ prepareCrossProfileDataSet();
+
+ dumpsysOnLogcat("Before save & load");
+
+ mService.saveDirtyInfo();
+ initService();
+
+ // First, all packages have less than 3 (== initial value) remaining calls.
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ // Simulate a call from sys UI.
+ mCallerPermissions.add(permission.RESET_SHORTCUT_MANAGER_THROTTLING);
+ mManager.onApplicationActive(CALLING_PACKAGE_1, USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mManager.onApplicationActive(CALLING_PACKAGE_3, USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+
+ mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_4, USER_0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+ MoreAsserts.assertNotEqual(3, mManager.getRemainingCallCount());
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertEquals(3, mManager.getRemainingCallCount());
+ });
+ }
+
+ public void testReportShortcutUsed() {
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ reset(mMockUsageStatsManagerInternal);
+
+ // Report with an nonexistent shortcut.
+ mManager.reportShortcutUsed("s1");
+ verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+ anyString(), anyString(), anyInt());
+
+ // Publish s2, but s1 still doesn't exist.
+ mManager.setDynamicShortcuts(list(makeShortcut("s2")));
+ mManager.reportShortcutUsed("s1");
+ verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+ anyString(), anyString(), anyInt());
+
+ mManager.reportShortcutUsed("s2");
+ verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage(
+ eq(CALLING_PACKAGE_1), eq("s2"), eq(USER_10));
+
+ });
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ // Try with a different package.
+ reset(mMockUsageStatsManagerInternal);
+
+ // Report with an nonexistent shortcut.
+ mManager.reportShortcutUsed("s2");
+ verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+ anyString(), anyString(), anyInt());
+
+ // Publish s2, but s1 still doesn't exist.
+ mManager.setDynamicShortcuts(list(makeShortcut("s3")));
+ mManager.reportShortcutUsed("s2");
+ verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+ anyString(), anyString(), anyInt());
+
+ mManager.reportShortcutUsed("s3");
+ verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage(
+ eq(CALLING_PACKAGE_2), eq("s3"), eq(USER_10));
+
+ });
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testGetResourcePackageName() {
+ assertEquals(null, ShortcutInfo.getResourcePackageName(""));
+ assertEquals(null, ShortcutInfo.getResourcePackageName("abc"));
+ assertEquals("p", ShortcutInfo.getResourcePackageName("p:"));
+ assertEquals("p", ShortcutInfo.getResourcePackageName("p:xx"));
+ assertEquals("pac", ShortcutInfo.getResourcePackageName("pac:"));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testGetResourceTypeName() {
+ assertEquals(null, ShortcutInfo.getResourceTypeName(""));
+ assertEquals(null, ShortcutInfo.getResourceTypeName(":"));
+ assertEquals(null, ShortcutInfo.getResourceTypeName("/"));
+ assertEquals(null, ShortcutInfo.getResourceTypeName("/:"));
+ assertEquals("a", ShortcutInfo.getResourceTypeName(":a/"));
+ assertEquals("type", ShortcutInfo.getResourceTypeName("xxx:type/yyy"));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testGetResourceTypeAndEntryName() {
+ assertEquals(null, ShortcutInfo.getResourceTypeAndEntryName(""));
+ assertEquals(null, ShortcutInfo.getResourceTypeAndEntryName("abc"));
+ assertEquals("", ShortcutInfo.getResourceTypeAndEntryName("p:"));
+ assertEquals("x", ShortcutInfo.getResourceTypeAndEntryName(":x"));
+ assertEquals("x", ShortcutInfo.getResourceTypeAndEntryName("p:x"));
+ assertEquals("xyz", ShortcutInfo.getResourceTypeAndEntryName("pac:xyz"));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testGetResourceEntryName() {
+ assertEquals(null, ShortcutInfo.getResourceEntryName(""));
+ assertEquals(null, ShortcutInfo.getResourceEntryName("ab:"));
+ assertEquals("", ShortcutInfo.getResourceEntryName("/"));
+ assertEquals("abc", ShortcutInfo.getResourceEntryName("/abc"));
+ assertEquals("abc", ShortcutInfo.getResourceEntryName("xyz/abc"));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testLookUpResourceName_systemResources() {
+ // For android system resources, lookUpResourceName will simply return the value as a
+ // string, regardless of "withType".
+ final Resources res = getTestContext().getResources();
+
+ assertEquals("" + android.R.string.cancel, ShortcutInfo.lookUpResourceName(res,
+ android.R.string.cancel, true, getTestContext().getPackageName()));
+ assertEquals("" + android.R.drawable.alert_dark_frame, ShortcutInfo.lookUpResourceName(res,
+ android.R.drawable.alert_dark_frame, true, getTestContext().getPackageName()));
+ assertEquals("" + android.R.string.cancel, ShortcutInfo.lookUpResourceName(res,
+ android.R.string.cancel, false, getTestContext().getPackageName()));
+ }
+
+ public void testLookUpResourceName_appResources() {
+ final Resources res = getTestContext().getResources();
+
+ assertEquals("shortcut_text1", ShortcutInfo.lookUpResourceName(res,
+ R.string.shortcut_text1, false, getTestContext().getPackageName()));
+ assertEquals("string/shortcut_text1", ShortcutInfo.lookUpResourceName(res,
+ R.string.shortcut_text1, true, getTestContext().getPackageName()));
+
+ assertEquals("black_16x64", ShortcutInfo.lookUpResourceName(res,
+ R.drawable.black_16x64, false, getTestContext().getPackageName()));
+ assertEquals("drawable/black_16x64", ShortcutInfo.lookUpResourceName(res,
+ R.drawable.black_16x64, true, getTestContext().getPackageName()));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testLookUpResourceId_systemResources() {
+ final Resources res = getTestContext().getResources();
+
+ assertEquals(android.R.string.cancel, ShortcutInfo.lookUpResourceId(res,
+ "" + android.R.string.cancel, null,
+ getTestContext().getPackageName()));
+ assertEquals(android.R.drawable.alert_dark_frame, ShortcutInfo.lookUpResourceId(res,
+ "" + android.R.drawable.alert_dark_frame, null,
+ getTestContext().getPackageName()));
+ }
+
+ // Test for a ShortcutInfo method.
+ public void testLookUpResourceId_appResources() {
+ final Resources res = getTestContext().getResources();
+
+ assertEquals(R.string.shortcut_text1,
+ ShortcutInfo.lookUpResourceId(res, "shortcut_text1", "string",
+ getTestContext().getPackageName()));
+
+ assertEquals(R.string.shortcut_text1,
+ ShortcutInfo.lookUpResourceId(res, "string/shortcut_text1", null,
+ getTestContext().getPackageName()));
+
+ assertEquals(R.drawable.black_16x64,
+ ShortcutInfo.lookUpResourceId(res, "black_16x64", "drawable",
+ getTestContext().getPackageName()));
+
+ assertEquals(R.drawable.black_16x64,
+ ShortcutInfo.lookUpResourceId(res, "drawable/black_16x64", null,
+ getTestContext().getPackageName()));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java
new file mode 100644
index 0000000..eb4db7a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java
@@ -0,0 +1,505 @@
+/*
+ * 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 static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
+
+import android.content.ComponentName;
+import android.content.pm.ShortcutInfo;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.frameworks.servicestests.R;
+import com.android.server.pm.ShortcutService.ConfigConstants;
+
+/**
+ * Tests related to shortcut rank auto-adjustment.
+ */
+@SmallTest
+public class ShortcutManagerTest3 extends BaseShortcutManagerTest {
+
+ private static final String CALLING_PACKAGE = CALLING_PACKAGE_1;
+
+ private static final ComponentName A1 = new ComponentName(CALLING_PACKAGE,
+ ShortcutActivity.class.getName());
+
+ private static final ComponentName A2 = new ComponentName(CALLING_PACKAGE,
+ ShortcutActivity2.class.getName());
+
+ private static final ComponentName A3 = new ComponentName(CALLING_PACKAGE,
+ ShortcutActivity3.class.getName());
+
+ private ShortcutInfo shortcut(String id, ComponentName activity, int rank) {
+ return makeShortcutWithActivityAndRank(id, activity, rank);
+ }
+
+ private ShortcutInfo shortcut(String id, ComponentName activity) {
+ return makeShortcutWithActivityAndRank(id, activity, ShortcutInfo.RANK_NOT_SET);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // We don't need throttling during this test class, and also relax the max cap.
+ mService.updateConfigurationLocked(
+ ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999,"
+ + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
+ );
+
+ setCaller(CALLING_PACKAGE, USER_0);
+ }
+
+ private void publishManifestShortcuts(ComponentName activity, int resId) {
+ addManifestShortcutResource(activity, resId);
+ updatePackageVersion(CALLING_PACKAGE, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE, USER_0));
+ }
+
+ public void testSetDynamicShortcuts_noManifestShortcuts() {
+ mManager.setDynamicShortcuts(list(
+ shortcut("s1", A1)
+ ));
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s1");
+
+ assertTrue(mManager.setDynamicShortcuts(list(
+ shortcut("s5", A1),
+ shortcut("s4", A1),
+ shortcut("s3", A1)
+ )));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s4", "s3");
+
+ // RANK_NOT_SET is always the last.
+ assertTrue(mManager.setDynamicShortcuts(list(
+ shortcut("s5", A1),
+ shortcut("s4", A1, 5),
+ shortcut("s3", A1, 3),
+ shortcut("s2", A1)
+ )));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s3", "s4", "s5", "s2");
+
+ // Same rank, preserve the argument order.
+ assertTrue(mManager.setDynamicShortcuts(list(
+ shortcut("s5", A1, 5),
+ shortcut("s4", A1, 0),
+ shortcut("s3", A1, 5)
+ )));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s4", "s5", "s3");
+
+ // Multiple activities.
+ assertTrue(mManager.setDynamicShortcuts(list(
+ shortcut("s5", A1),
+ shortcut("s4", A2),
+ shortcut("s3", A3)
+ )));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5");
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("s4");
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A3)
+ .haveRanksInOrder("s3");
+
+ assertTrue(mManager.setDynamicShortcuts(list(
+ shortcut("s5", A1, 5),
+ shortcut("s4", A1),
+ shortcut("s3", A1, 5),
+ shortcut("x5", A2, 5),
+ shortcut("x4", A2),
+ shortcut("x3", A2, 1)
+ )));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s4");
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x5", "x4");
+
+ // Clear. Make sure it wouldn't lead to invalid internals state.
+ // (ShortcutService.verifyStates() will do so internally.)
+ assertTrue(mManager.setDynamicShortcuts(list()));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1).isEmpty();
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2).isEmpty();
+ }
+
+ private void runTestWithManifestShortcuts(Runnable r) {
+ publishManifestShortcuts(A1, R.xml.shortcut_5_alt);
+ publishManifestShortcuts(A2, R.xml.shortcut_1);
+
+ assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A1)
+ .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt");
+
+ assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A2)
+ .haveRanksInOrder("ms1");
+
+ // Existence of manifest shortcuts shouldn't affect dynamic shortcut ranks,
+ // so running another test here should pass.
+ r.run();
+
+ // And dynamic shortcut tests shouldn't affect manifest shortcuts, so repeat the
+ // same check.
+ assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A1)
+ .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt");
+
+ assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A2)
+ .haveRanksInOrder("ms1");
+ }
+
+ public void testSetDynamicShortcuts_withManifestShortcuts() {
+ runTestWithManifestShortcuts(() -> testSetDynamicShortcuts_noManifestShortcuts());
+ }
+
+ public void testAddDynamicShortcuts_noManifestShortcuts() {
+ mManager.addDynamicShortcuts(list(
+ shortcut("s1", A1)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s1");
+
+ //------------------------------------------------------
+ long lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.addDynamicShortcuts(list(
+ shortcut("s5", A1, 0),
+ shortcut("s4", A1),
+ shortcut("s2", A1, 3),
+ shortcut("x1", A2),
+ shortcut("x3", A2, 2),
+ shortcut("x2", A2, 2),
+ shortcut("s3", A1, 0)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s1", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s5", "s3", "s1", "s2", "s4", "x3", "x2", "x1");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.addDynamicShortcuts(list(
+ shortcut("s1", A1, 1)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s1", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s1", "s3");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.addDynamicShortcuts(list(
+ shortcut("s1", A1, 1),
+
+ // This is add, not update, so the following means s5 will have NO_RANK,
+ // which puts it at the end.
+ shortcut("s5", A1),
+ shortcut("s3", A1, 0),
+
+ // s10 also has NO_RANK, so it'll be put at the end, even after "s5" as we preserve
+ // the argument order.
+ shortcut("s10", A1),
+
+ // Note we're changing the activity for x2.
+ shortcut("x2", A1, 0),
+ shortcut("x10", A2)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s3", "x2", "s1", "s2", "s4", "s5", "s10");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x1", "x10");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s3", "x2", "s1", "s5", "s10", "x1", "x10");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ // Change the activities again.
+ mManager.addDynamicShortcuts(list(
+ shortcut("s1", A2),
+ shortcut("s2", A2, 999)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s3", "x2", "s4", "s5", "s10");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x1", "x10", "s2", "s1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s1", "s2", "s4", "s5", "s10");
+ }
+
+ public void testAddDynamicShortcuts_withManifestShortcuts() {
+ runTestWithManifestShortcuts(() -> testAddDynamicShortcuts_noManifestShortcuts());
+ }
+
+ public void testUpdateShortcuts_noManifestShortcuts() {
+ mManager.addDynamicShortcuts(list(
+ shortcut("s5", A1, 0),
+ shortcut("s4", A1),
+ shortcut("s2", A1, 3),
+ shortcut("x1", A2),
+ shortcut("x3", A2, 2),
+ shortcut("x2", A2, 2),
+ shortcut("s3", A1, 0)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ //------------------------------------------------------
+ long lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.updateShortcuts(list());
+ // Same order.
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .isEmpty();
+
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE, list("s2", "s4", "x2"), HANDLE_USER_0);
+ });
+ // Still same order.
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.updateShortcuts(list(
+ shortcut("s4", A1, 1),
+
+ // Rank not changing, should keep the same positions.
+ // c.f. in case of addDynamicShortcuts, this means "put them at the end".
+ shortcut("s3", A1),
+ shortcut("x2", A2)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s4", "s3", "s2");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s4", "s3", "s2", "x2");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.updateShortcuts(list(
+ shortcut("s4", A1, 0),
+
+ // Change the activity without specifying a rank -> keep the same rank.
+ shortcut("s5", A2),
+
+ // Change the activity without specifying a rank -> assign a new rank.
+ shortcut("x2", A1, 2),
+
+ // "xx" doesn't exist, so it'll be ignored.
+ shortcut("xx", A1, 0)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s4", "x2", "s3", "s2");
+
+ // Interesting case: both x3 and s5 originally had rank=0, and in this case s5 has moved
+ // to A2 without changing the rank. So they're tie for the new rank, as well as
+ // the "rank changed" bit. Also in this case, "s5" won't have an implicit order, since
+ // its rank isn't changing. So we sort them by ID, thus s5 comes before x3.
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("s5", "x3", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s4", "x2", "s5", "x3");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.updateShortcuts(list(
+ shortcut("s3", A3)));
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s4", "x2", "s2");
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("s5", "x3", "x1");
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A3)
+ .haveRanksInOrder("s3");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s3", "s2");
+ }
+
+ public void testUpdateShortcuts_withManifestShortcuts() {
+ runTestWithManifestShortcuts(() -> testUpdateShortcuts_noManifestShortcuts());
+ }
+
+ public void testDeleteDynamicShortcuts_noManifestShortcuts() {
+ mManager.addDynamicShortcuts(list(
+ shortcut("s5", A1, 0),
+ shortcut("s4", A1),
+ shortcut("s2", A1, 3),
+ shortcut("x1", A2),
+ shortcut("x3", A2, 2),
+ shortcut("x2", A2, 2),
+ shortcut("s3", A1, 0)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ //------------------------------------------------------
+ long lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.removeDynamicShortcuts(list());
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .isEmpty();
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(
+ CALLING_PACKAGE, list("s2", "s4", "x1", "x2"), HANDLE_USER_0);
+ });
+ // Still same order.
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.removeDynamicShortcuts(list("s3", "x1", "xxxx"));
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s2", "s4");
+ }
+
+ public void testDeleteDynamicShortcuts_withManifestShortcuts() {
+ runTestWithManifestShortcuts(() -> testDeleteDynamicShortcuts_noManifestShortcuts());
+ }
+
+ public void testDisableShortcuts_noManifestShortcuts() {
+ mManager.addDynamicShortcuts(list(
+ shortcut("s5", A1, 0),
+ shortcut("s4", A1),
+ shortcut("s2", A1, 3),
+ shortcut("x1", A2),
+ shortcut("x3", A2, 2),
+ shortcut("x2", A2, 2),
+ shortcut("s3", A1, 0)
+ ));
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ //------------------------------------------------------
+ long lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.disableShortcuts(list());
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s3", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2", "x1");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .isEmpty();
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.disableShortcuts(list("s3", "x1", "xxxx"));
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s2", "s4");
+
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE, list("s2", "s4", "x2"), HANDLE_USER_0);
+ });
+ // Still same order.
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s2", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x3", "x2");
+
+ //------------------------------------------------------
+ lastApiTime = ++mInjectedCurrentTimeMillis;
+
+ mManager.disableShortcuts(list("s2", "x3"));
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1)
+ .haveRanksInOrder("s5", "s4");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2)
+ .haveRanksInOrder("x2");
+
+ assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime)
+ .haveIds("s4", "x2");
+ }
+
+ public void testDisableShortcuts_withManifestShortcuts() {
+ runTestWithManifestShortcuts(() -> testDisableShortcuts_noManifestShortcuts());
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 48a11c5..9f77297 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -23,12 +23,14 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
import android.util.AtomicFile;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
+@SmallTest
public class UserManagerServiceTest extends AndroidTestCase {
private static String[] STRING_ARRAY = new String[] {"<tag", "<![CDATA["};
private File restrictionsFile;
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 6c2fcd5..ced4980 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -25,6 +25,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
import com.android.internal.util.ArrayUtils;
@@ -33,6 +34,7 @@
import java.util.List;
/** Test {@link UserManager} functionality. */
+@MediumTest
public class UserManagerTest extends AndroidTestCase {
private static final int REMOVE_CHECK_INTERVAL = 500;
private static final int REMOVE_TIMEOUT = 60 * 1000;
@@ -96,7 +98,7 @@
&& !user.isPrimary()) {
found = true;
Bundle restrictions = mUserManager.getUserRestrictions(user.getUserHandle());
- assertFalse("New user should have DISALLOW_CONFIG_WIFI =false by default",
+ assertTrue("Guest user should have DISALLOW_CONFIG_WIFI=true by default",
restrictions.getBoolean(UserManager.DISALLOW_CONFIG_WIFI));
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java b/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java
index 5bdf6f7..e2dce853 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java
@@ -23,6 +23,7 @@
import android.os.UserManager;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
/**
* Tests for {@link com.android.server.pm.UserRestrictionsUtils}.
@@ -35,6 +36,7 @@
-w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
* </pre>
*/
+@SmallTest
public class UserRestrictionsUtilsTest extends AndroidTestCase {
public void testNonNull() {
Bundle out = UserRestrictionsUtils.nonNull(null);
diff --git a/services/tests/shortcutmanagerutils/Android.mk b/services/tests/shortcutmanagerutils/Android.mk
new file mode 100644
index 0000000..701e058
--- /dev/null
+++ b/services/tests/shortcutmanagerutils/Android.mk
@@ -0,0 +1,31 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ mockito-target
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := ShortcutManagerTestUtils
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
new file mode 100644
index 0000000..d9dbd5a
--- /dev/null
+++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
@@ -0,0 +1,854 @@
+/*
+ * 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.shortcutmanagertest;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyList;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.test.MoreAsserts;
+import android.util.ArraySet;
+import android.util.Log;
+
+import junit.framework.Assert;
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.mockito.Mockito;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Common utility methods for ShortcutManager tests. This is used by both CTS and the unit tests.
+ * Because it's used by CTS too, it can only access the public APIs.
+ */
+public class ShortcutManagerTestUtils {
+ private static final String TAG = "ShortcutManagerUtils";
+
+ private static final boolean ENABLE_DUMPSYS = false; // DO NOT SUBMIT WITH true
+
+ private static final int STANDARD_TIMEOUT_SEC = 5;
+
+ private static final String[] EMPTY_STRINGS = new String[0];
+
+ private ShortcutManagerTestUtils() {
+ }
+
+ private static List<String> readAll(ParcelFileDescriptor pfd) {
+ try {
+ try {
+ final ArrayList<String> ret = new ArrayList<>();
+ try (BufferedReader r = new BufferedReader(
+ new FileReader(pfd.getFileDescriptor()))) {
+ String line;
+ while ((line = r.readLine()) != null) {
+ ret.add(line);
+ }
+ r.readLine();
+ }
+ return ret;
+ } finally {
+ pfd.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String concatResult(List<String> result) {
+ final StringBuilder sb = new StringBuilder();
+ for (String s : result) {
+ sb.append(s);
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+
+ private static List<String> runCommand(Instrumentation instrumentation, String command) {
+ return runCommand(instrumentation, command, null);
+ }
+ private static List<String> runCommand(Instrumentation instrumentation, String command,
+ Predicate<List<String>> resultAsserter) {
+ Log.d(TAG, "Running command: " + command);
+ final List<String> result;
+ try {
+ result = readAll(
+ instrumentation.getUiAutomation().executeShellCommand(command));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ if (resultAsserter != null && !resultAsserter.test(result)) {
+ fail("Command '" + command + "' failed, output was:\n" + concatResult(result));
+ }
+ return result;
+ }
+
+ private static void runCommandForNoOutput(Instrumentation instrumentation, String command) {
+ runCommand(instrumentation, command, result -> result.size() == 0);
+ }
+
+ private static List<String> runShortcutCommand(Instrumentation instrumentation, String command,
+ Predicate<List<String>> resultAsserter) {
+ return runCommand(instrumentation, "cmd shortcut " + command, resultAsserter);
+ }
+
+ public static List<String> runShortcutCommandForSuccess(Instrumentation instrumentation,
+ String command) {
+ return runShortcutCommand(instrumentation, command, result -> result.contains("Success"));
+ }
+
+ public static String getDefaultLauncher(Instrumentation instrumentation) {
+ final String PREFIX = "Launcher: ComponentInfo{";
+ final String POSTFIX = "}";
+ final List<String> result = runShortcutCommandForSuccess(
+ instrumentation, "get-default-launcher");
+ for (String s : result) {
+ if (s.startsWith(PREFIX) && s.endsWith(POSTFIX)) {
+ return s.substring(PREFIX.length(), s.length() - POSTFIX.length());
+ }
+ }
+ fail("Default launcher not found");
+ return null;
+ }
+
+ public static void setDefaultLauncher(Instrumentation instrumentation, String component) {
+ runCommandForNoOutput(instrumentation, "cmd package set-home-activity " + component);
+ }
+
+ public static void setDefaultLauncher(Instrumentation instrumentation, Context packageContext) {
+ setDefaultLauncher(instrumentation, packageContext.getPackageName()
+ + "/android.content.pm.cts.shortcutmanager.packages.Launcher");
+ }
+
+ public static void overrideConfig(Instrumentation instrumentation, String config) {
+ runShortcutCommandForSuccess(instrumentation, "override-config " + config);
+ }
+
+ public static void resetConfig(Instrumentation instrumentation) {
+ runShortcutCommandForSuccess(instrumentation, "reset-config");
+ }
+
+ public static void resetThrottling(Instrumentation instrumentation) {
+ runShortcutCommandForSuccess(instrumentation, "reset-throttling");
+ }
+
+ public static void resetAllThrottling(Instrumentation instrumentation) {
+ runShortcutCommandForSuccess(instrumentation, "reset-all-throttling");
+ }
+
+ public static void clearShortcuts(Instrumentation instrumentation, int userId,
+ String packageName) {
+ runShortcutCommandForSuccess(instrumentation, "clear-shortcuts "
+ + " --user " + userId + " " + packageName);
+ }
+
+ public static void dumpsysShortcut(Instrumentation instrumentation) {
+ if (!ENABLE_DUMPSYS) {
+ return;
+ }
+ for (String s : runCommand(instrumentation, "dumpsys shortcut")) {
+ Log.e(TAG, s);
+ }
+ }
+
+ public static Bundle makeBundle(Object... keysAndValues) {
+ assertTrue((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;
+ }
+
+ public static <T> List<T> list(T... array) {
+ return Arrays.asList(array);
+ }
+
+ public static <T> Set<T> hashSet(Set<T> in) {
+ return new LinkedHashSet<>(in);
+ }
+
+ public static <T> Set<T> set(T... values) {
+ return set(v -> v, values);
+ }
+
+ public static <T, V> Set<T> set(Function<V, T> converter, V... values) {
+ return set(converter, Arrays.asList(values));
+ }
+
+ public static <T, V> Set<T> set(Function<V, T> converter, List<V> values) {
+ final LinkedHashSet<T> ret = new LinkedHashSet<>();
+ for (V v : values) {
+ ret.add(converter.apply(v));
+ }
+ return ret;
+ }
+
+ public static void resetAll(Collection<?> mocks) {
+ for (Object o : mocks) {
+ reset(o);
+ }
+ }
+
+ public static <T extends Collection<?>> T assertEmpty(T collection) {
+ if (collection == null) {
+ return collection; // okay.
+ }
+ assertEquals(0, collection.size());
+ return collection;
+ }
+
+ public static List<ShortcutInfo> filter(List<ShortcutInfo> list, Predicate<ShortcutInfo> p) {
+ final ArrayList<ShortcutInfo> ret = new ArrayList<>(list);
+ ret.removeIf(si -> !p.test(si));
+ return ret;
+ }
+
+ public static List<ShortcutInfo> filterByActivity(List<ShortcutInfo> list,
+ ComponentName activity) {
+ return filter(list, si ->
+ (si.getActivity().equals(activity)
+ && (si.isManifestShortcut() || si.isDynamic())));
+ }
+
+ public static List<ShortcutInfo> changedSince(List<ShortcutInfo> list, long time) {
+ return filter(list, si -> si.getLastChangedTimestamp() >= time);
+ }
+
+ public static void assertExpectException(Class<? extends Throwable> expectedExceptionType,
+ String expectedExceptionMessageRegex, Runnable r) {
+ assertExpectException("", expectedExceptionType, expectedExceptionMessageRegex, r);
+ }
+
+ public static void assertCannotUpdateImmutable(Runnable r) {
+ assertExpectException(
+ IllegalArgumentException.class, "may not be manipulated via APIs", r);
+ }
+
+ public static void assertDynamicShortcutCountExceeded(Runnable r) {
+ assertExpectException(IllegalArgumentException.class,
+ "Max number of dynamic shortcuts exceeded", r);
+ }
+
+ public static void assertExpectException(String message,
+ Class<? extends Throwable> expectedExceptionType,
+ String expectedExceptionMessageRegex, Runnable r) {
+ try {
+ r.run();
+ } catch (Throwable e) {
+ Assert.assertTrue(
+ "Expected exception type was " + expectedExceptionType.getName()
+ + " but caught " + e + " (message=" + message + ")",
+ expectedExceptionType.isAssignableFrom(e.getClass()));
+ if (expectedExceptionMessageRegex != null) {
+ MoreAsserts.assertContainsRegex(expectedExceptionMessageRegex, e.getMessage());
+ }
+ return; // Pass
+ }
+ Assert.fail("Expected exception type " + expectedExceptionType.getName()
+ + " was not thrown");
+ }
+
+ public static List<ShortcutInfo> assertShortcutIds(List<ShortcutInfo> actualShortcuts,
+ String... expectedIds) {
+ final SortedSet<String> expected = new TreeSet<>(list(expectedIds));
+ final SortedSet<String> actual = new TreeSet<>();
+ for (ShortcutInfo s : actualShortcuts) {
+ actual.add(s.getId());
+ }
+
+ // Compare the sets.
+ assertEquals(expected, actual);
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertShortcutIdsOrdered(List<ShortcutInfo> actualShortcuts,
+ String... expectedIds) {
+ final ArrayList<String> expected = new ArrayList<>(list(expectedIds));
+ final ArrayList<String> actual = new ArrayList<>();
+ for (ShortcutInfo s : actualShortcuts) {
+ actual.add(s.getId());
+ }
+ assertEquals(expected, actual);
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllHaveIntents(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertNotNull("ID " + s.getId(), s.getIntent());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllNotHaveIntents(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertNull("ID " + s.getId(), s.getIntent());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllHaveTitle(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertNotNull("ID " + s.getId(), s.getShortLabel());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllNotHaveTitle(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertNull("ID " + s.getId(), s.getShortLabel());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllHaveIconResId(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId() + " not have icon res ID", s.hasIconResource());
+ assertFalse("ID " + s.getId() + " shouldn't have icon FD", s.hasIconFile());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllHaveIconFile(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertFalse("ID " + s.getId() + " shouldn't have icon res ID", s.hasIconResource());
+ assertTrue("ID " + s.getId() + " not have icon FD", s.hasIconFile());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllHaveIcon(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId() + " has no icon ", s.hasIconFile() || s.hasIconResource());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllKeyFieldsOnly(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.hasKeyFieldsOnly());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllNotKeyFieldsOnly(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertFalse("ID " + s.getId(), s.hasKeyFieldsOnly());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllDynamic(List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isDynamic());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllPinned(List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isPinned());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllDynamicOrPinned(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isDynamic() || s.isPinned());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllManifest(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isManifestShortcut());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllNotManifest(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertFalse("ID " + s.getId(), s.isManifestShortcut());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllDisabled(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), !s.isEnabled());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllEnabled(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isEnabled());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllImmutable(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.isImmutable());
+ }
+ return actualShortcuts;
+ }
+
+ public static List<ShortcutInfo> assertAllStringsResolved(
+ List<ShortcutInfo> actualShortcuts) {
+ for (ShortcutInfo s : actualShortcuts) {
+ assertTrue("ID " + s.getId(), s.hasStringResourcesResolved());
+ }
+ return actualShortcuts;
+ }
+
+ public static void assertDynamicOnly(ShortcutInfo si) {
+ assertTrue(si.isDynamic());
+ assertFalse(si.isPinned());
+ }
+
+ public static void assertPinnedOnly(ShortcutInfo si) {
+ assertFalse(si.isDynamic());
+ assertFalse(si.isManifestShortcut());
+ assertTrue(si.isPinned());
+ }
+
+ public static void assertDynamicAndPinned(ShortcutInfo si) {
+ assertTrue(si.isDynamic());
+ assertTrue(si.isPinned());
+ }
+
+ public static void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
+ assertEquals("width", expectedWidth, bitmap.getWidth());
+ assertEquals("height", expectedHeight, bitmap.getHeight());
+ }
+
+ public static <T> void assertAllUnique(Collection<T> list) {
+ final Set<Object> set = new LinkedHashSet<>();
+ for (T item : list) {
+ if (set.contains(item)) {
+ fail("Duplicate item found: " + item + " (in the list: " + list + ")");
+ }
+ set.add(item);
+ }
+ }
+
+ public static ShortcutInfo findShortcut(List<ShortcutInfo> list, String id) {
+ for (ShortcutInfo si : list) {
+ if (si.getId().equals(id)) {
+ return si;
+ }
+ }
+ fail("Shortcut " + id + " not found in the list");
+ return null;
+ }
+
+ public static Bitmap pfdToBitmap(ParcelFileDescriptor pfd) {
+ assertNotNull(pfd);
+ try {
+ try {
+ return BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
+ } finally {
+ pfd.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void assertBundleEmpty(BaseBundle b) {
+ assertTrue(b == null || b.size() == 0);
+ }
+
+ public static void assertCallbackNotReceived(LauncherApps.Callback mock) {
+ verify(mock, times(0)).onShortcutsChanged(anyString(), anyList(),
+ any(UserHandle.class));
+ }
+
+ public static void assertCallbackReceived(LauncherApps.Callback mock,
+ UserHandle user, String packageName, String... ids) {
+ verify(mock).onShortcutsChanged(eq(packageName), checkShortcutIds(ids),
+ eq(user));
+ }
+
+ public static boolean checkAssertSuccess(Runnable r) {
+ try {
+ r.run();
+ return true;
+ } catch (AssertionError e) {
+ return false;
+ }
+ }
+
+ public static <T> T checkArgument(Predicate<T> checker, String description,
+ List<T> matchedCaptor) {
+ final Matcher<T> m = new BaseMatcher<T>() {
+ @Override
+ public boolean matches(Object item) {
+ if (item == null) {
+ return false;
+ }
+ final T value = (T) item;
+ if (!checker.test(value)) {
+ return false;
+ }
+
+ if (matchedCaptor != null) {
+ matchedCaptor.add(value);
+ }
+ return true;
+ }
+
+ @Override
+ public void describeTo(Description d) {
+ d.appendText(description);
+ }
+ };
+ return Mockito.argThat(m);
+ }
+
+ public static List<ShortcutInfo> checkShortcutIds(String... ids) {
+ return checkArgument((List<ShortcutInfo> list) -> {
+ final Set<String> actualSet = set(si -> si.getId(), list);
+ return actualSet.equals(set(ids));
+
+ }, "Shortcut IDs=[" + Arrays.toString(ids) + "]", null);
+ }
+
+ public static ShortcutInfo parceled(ShortcutInfo si) {
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(si, 0);
+ p.setDataPosition(0);
+ ShortcutInfo si2 = p.readParcelable(ShortcutManagerTestUtils.class.getClassLoader());
+ p.recycle();
+ return si2;
+ }
+
+ public static List<ShortcutInfo> cloneShortcutList(List<ShortcutInfo> list) {
+ if (list == null) {
+ return null;
+ }
+ final List<ShortcutInfo> ret = new ArrayList<>(list.size());
+ for (ShortcutInfo si : list) {
+ ret.add(parceled(si));
+ }
+
+ return ret;
+ }
+
+ private static final Comparator<ShortcutInfo> sRankComparator =
+ (ShortcutInfo a, ShortcutInfo b) -> Integer.compare(a.getRank(), b.getRank());
+
+ public static List<ShortcutInfo> sortedByRank(List<ShortcutInfo> shortcuts) {
+ final ArrayList<ShortcutInfo> ret = new ArrayList<>(shortcuts);
+ Collections.sort(ret, sRankComparator);
+ return ret;
+ }
+
+ public static void waitUntil(String message, BooleanSupplier condition) {
+ waitUntil(message, condition, STANDARD_TIMEOUT_SEC);
+ }
+
+ public static void waitUntil(String message, BooleanSupplier condition, int timeoutSeconds) {
+ final long timeout = System.currentTimeMillis() + (timeoutSeconds * 1000L);
+ while (System.currentTimeMillis() < timeout) {
+ if (condition.getAsBoolean()) {
+ return;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ fail("Timed out for: " + message);
+ }
+
+ public static ShortcutListAsserter assertWith(List<ShortcutInfo> list) {
+ return new ShortcutListAsserter(list);
+ }
+
+ /**
+ * New style assertion that allows chained calls.
+ */
+ public static class ShortcutListAsserter {
+ private final ShortcutListAsserter mOriginal;
+ private final List<ShortcutInfo> mList;
+
+ ShortcutListAsserter(List<ShortcutInfo> list) {
+ this(null, list);
+ }
+
+ private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) {
+ mOriginal = original == null ? this : original;
+ mList = new ArrayList<>(list);
+ }
+
+ public ShortcutListAsserter revertToOriginalList() {
+ return mOriginal;
+ }
+
+ public ShortcutListAsserter selectDynamic() {
+ return new ShortcutListAsserter(this,
+ filter(mList, ShortcutInfo::isDynamic));
+ }
+
+ public ShortcutListAsserter selectManifest() {
+ return new ShortcutListAsserter(this,
+ filter(mList, ShortcutInfo::isManifestShortcut));
+ }
+
+ public ShortcutListAsserter selectPinned() {
+ return new ShortcutListAsserter(this,
+ filter(mList, ShortcutInfo::isPinned));
+ }
+
+ public ShortcutListAsserter selectByActivity(ComponentName activity) {
+ return new ShortcutListAsserter(this,
+ ShortcutManagerTestUtils.filterByActivity(mList, activity));
+ }
+
+ public ShortcutListAsserter selectByChangedSince(long time) {
+ return new ShortcutListAsserter(this,
+ ShortcutManagerTestUtils.changedSince(mList, time));
+ }
+
+ public ShortcutListAsserter selectByIds(String... ids) {
+ final Set<String> idSet = set(ids);
+ final ArrayList<ShortcutInfo> selected = new ArrayList<>();
+ for (ShortcutInfo si : mList) {
+ if (idSet.contains(si.getId())) {
+ selected.add(si);
+ idSet.remove(si.getId());
+ }
+ }
+ if (idSet.size() > 0) {
+ fail("Shortcuts not found for IDs=" + idSet);
+ }
+
+ return new ShortcutListAsserter(this, selected);
+ }
+
+ public ShortcutListAsserter toSortByRank() {
+ return new ShortcutListAsserter(this,
+ ShortcutManagerTestUtils.sortedByRank(mList));
+ }
+
+ public ShortcutListAsserter call(Consumer<List<ShortcutInfo>> c) {
+ c.accept(mList);
+ return this;
+ }
+
+ public ShortcutListAsserter haveIds(String... expectedIds) {
+ assertShortcutIds(mList, expectedIds);
+ return this;
+ }
+
+ public ShortcutListAsserter haveIdsOrdered(String... expectedIds) {
+ assertShortcutIdsOrdered(mList, expectedIds);
+ return this;
+ }
+
+ private ShortcutListAsserter haveSequentialRanks() {
+ for (int i = 0; i < mList.size(); i++) {
+ final ShortcutInfo si = mList.get(i);
+ assertEquals("Rank not sequential: id=" + si.getId(), i, si.getRank());
+ }
+ return this;
+ }
+
+ public ShortcutListAsserter haveRanksInOrder(String... expectedIds) {
+ toSortByRank()
+ .haveSequentialRanks()
+ .haveIdsOrdered(expectedIds);
+ return this;
+ }
+
+ public ShortcutListAsserter isEmpty() {
+ assertEquals(0, mList.size());
+ return this;
+ }
+
+ public ShortcutListAsserter areAllDynamic() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isDynamic()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllNotDynamic() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isDynamic()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllPinned() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isPinned()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllNotPinned() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isPinned()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllManifest() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isManifestShortcut()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllNotManifest() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isManifestShortcut()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllImmutable() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isImmutable()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllMutable() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isImmutable()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllEnabled() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isEnabled()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllDisabled() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isEnabled()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllWithKeyFieldsOnly() {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.hasKeyFieldsOnly()));
+ return this;
+ }
+
+ public ShortcutListAsserter areAllNotWithKeyFieldsOnly() {
+ forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.hasKeyFieldsOnly()));
+ return this;
+ }
+
+ public ShortcutListAsserter forAllShortcuts(Consumer<ShortcutInfo> sa) {
+ boolean found = false;
+ for (int i = 0; i < mList.size(); i++) {
+ final ShortcutInfo si = mList.get(i);
+ found = true;
+ sa.accept(si);
+ }
+ assertTrue("No shortcuts found.", found);
+ return this;
+ }
+
+ public ShortcutListAsserter forShortcut(Predicate<ShortcutInfo> p,
+ Consumer<ShortcutInfo> sa) {
+ boolean found = false;
+ for (int i = 0; i < mList.size(); i++) {
+ final ShortcutInfo si = mList.get(i);
+ if (p.test(si)) {
+ found = true;
+ try {
+ sa.accept(si);
+ } catch (Throwable e) {
+ throw new AssertionError("Assertion failed for shortcut " + si.getId(), e);
+ }
+ }
+ }
+ assertTrue("Shortcut with the given condition not found.", found);
+ return this;
+ }
+
+ public ShortcutListAsserter forShortcutWithId(String id, Consumer<ShortcutInfo> sa) {
+ forShortcut(si -> si.getId().equals(id), sa);
+
+ return this;
+ }
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index ecfeff9..eb3c665 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1412,6 +1412,24 @@
}
@Override
+ public void reportShortcutUsage(String packageName, String shortcutId, int userId) {
+ if (packageName == null || shortcutId == null) {
+ Slog.w(TAG, "Event reported without a package name or a shortcut ID");
+ return;
+ }
+
+ UsageEvents.Event event = new UsageEvents.Event();
+ event.mPackage = packageName.intern();
+ event.mShortcutId = shortcutId.intern();
+
+ // This will later be converted to system time.
+ event.mTimeStamp = SystemClock.elapsedRealtime();
+
+ event.mEventType = Event.SHORTCUT_INVOCATION;
+ mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ }
+
+ @Override
public void reportContentProviderUsage(String name, String packageName, int userId) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = name;
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
index c95ff23..03cee9c 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
@@ -51,6 +51,7 @@
private static final String ACTIVE_ATTR = "active";
private static final String LAST_EVENT_ATTR = "lastEvent";
private static final String TYPE_ATTR = "type";
+ private static final String SHORTCUT_ID_ATTR = "shortcutId";
// Time attributes stored as an offset of the beginTime.
private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
@@ -106,9 +107,15 @@
event.mTimeStamp = statsOut.beginTime + XmlUtils.readLongAttribute(parser, TIME_ATTR);
event.mEventType = XmlUtils.readIntAttribute(parser, TYPE_ATTR);
- if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
- event.mConfiguration = new Configuration();
- Configuration.readXmlAttrs(parser, event.mConfiguration);
+ switch (event.mEventType) {
+ case UsageEvents.Event.CONFIGURATION_CHANGE:
+ event.mConfiguration = new Configuration();
+ Configuration.readXmlAttrs(parser, event.mConfiguration);
+ break;
+ case UsageEvents.Event.SHORTCUT_INVOCATION:
+ final String id = XmlUtils.readStringAttribute(parser, SHORTCUT_ID_ATTR);
+ event.mShortcutId = (id != null) ? id.intern() : null;
+ break;
}
if (statsOut.events == null) {
@@ -165,9 +172,17 @@
}
XmlUtils.writeIntAttribute(xml, TYPE_ATTR, event.mEventType);
- if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE
- && event.mConfiguration != null) {
- Configuration.writeXmlAttrs(xml, event.mConfiguration);
+ switch (event.mEventType) {
+ case UsageEvents.Event.CONFIGURATION_CHANGE:
+ if (event.mConfiguration != null) {
+ Configuration.writeXmlAttrs(xml, event.mConfiguration);
+ }
+ break;
+ case UsageEvents.Event.SHORTCUT_INVOCATION:
+ if (event.mShortcutId != null) {
+ XmlUtils.writeStringAttribute(xml, SHORTCUT_ID_ATTR, event.mShortcutId);
+ }
+ break;
}
xml.endTag(null, EVENT_TAG);
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 7d003f3..59e4c80 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -549,6 +549,9 @@
if (event.mConfiguration != null) {
pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
}
+ if (event.mShortcutId != null) {
+ pw.printPair("shortcutId", event.mShortcutId);
+ }
pw.println();
}
pw.decreaseIndent();
@@ -588,6 +591,8 @@
return "SYSTEM_INTERACTION";
case UsageEvents.Event.USER_INTERACTION:
return "USER_INTERACTION";
+ case UsageEvents.Event.SHORTCUT_INVOCATION:
+ return "SHORTCUT_INVOCATION";
default:
return "UNKNOWN";
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java
index 0f68cca..0dcd152 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java
@@ -40,11 +40,12 @@
static final boolean DBG = false;
private static final String NAME = "sound_model.db";
- private static final int VERSION = 4;
+ private static final int VERSION = 5;
public static interface SoundModelContract {
public static final String TABLE = "sound_model";
public static final String KEY_MODEL_UUID = "model_uuid";
+ public static final String KEY_VENDOR_UUID = "vendor_uuid";
public static final String KEY_KEYPHRASE_ID = "keyphrase_id";
public static final String KEY_TYPE = "type";
public static final String KEY_DATA = "data";
@@ -58,6 +59,7 @@
private static final String CREATE_TABLE_SOUND_MODEL = "CREATE TABLE "
+ SoundModelContract.TABLE + "("
+ SoundModelContract.KEY_MODEL_UUID + " TEXT PRIMARY KEY,"
+ + SoundModelContract.KEY_VENDOR_UUID + " TEXT, "
+ SoundModelContract.KEY_KEYPHRASE_ID + " INTEGER,"
+ SoundModelContract.KEY_TYPE + " INTEGER,"
+ SoundModelContract.KEY_DATA + " BLOB,"
@@ -78,9 +80,19 @@
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // TODO: For now, drop older tables and recreate new ones.
- db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
- onCreate(db);
+ if (oldVersion < 4) {
+ // For old versions just drop the tables and recreate new ones.
+ db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
+ onCreate(db);
+ } else {
+ // In the jump to version 5, we added support for the vendor UUID.
+ if (oldVersion == 4) {
+ Slog.d(TAG, "Adding vendor UUID column");
+ db.execSQL("ALTER TABLE " + SoundModelContract.TABLE + " ADD COLUMN "
+ + SoundModelContract.KEY_VENDOR_UUID + " TEXT");
+ oldVersion++;
+ }
+ }
}
/**
@@ -93,6 +105,9 @@
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SoundModelContract.KEY_MODEL_UUID, soundModel.uuid.toString());
+ if (soundModel.vendorUuid != null) {
+ values.put(SoundModelContract.KEY_VENDOR_UUID, soundModel.vendorUuid.toString());
+ }
values.put(SoundModelContract.KEY_TYPE, SoundTrigger.SoundModel.TYPE_KEYPHRASE);
values.put(SoundModelContract.KEY_DATA, soundModel.data);
@@ -176,6 +191,11 @@
continue;
}
+ String vendorUuidString = null;
+ int vendorUuidColumn = c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID);
+ if (vendorUuidColumn != -1) {
+ vendorUuidString = c.getString(vendorUuidColumn);
+ }
byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
int recognitionModes = c.getInt(
c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
@@ -212,9 +232,12 @@
Keyphrase[] keyphrases = new Keyphrase[1];
keyphrases[0] = new Keyphrase(
keyphraseId, recognitionModes, modelLocale, text, users);
+ UUID vendorUuid = null;
+ if (vendorUuidString != null) {
+ vendorUuid = UUID.fromString(vendorUuidString);
+ }
KeyphraseSoundModel model = new KeyphraseSoundModel(
- UUID.fromString(modelUuid),
- null /* FIXME use vendor UUID */, data, keyphrases);
+ UUID.fromString(modelUuid), vendorUuid, data, keyphrases);
if (DBG) {
Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: "
+ model);
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 7d7e1eb..3a6d1fd 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -23,6 +23,7 @@
import java.lang.String;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -104,7 +105,6 @@
* An {@link InCallService} will only see this state if it has the
* {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} in its
* manifest.
- * @hide
*/
public static final int STATE_PULLING_CALL = 11;
@@ -252,7 +252,6 @@
* <p>
* See {@link Connection#CAPABILITY_CAN_PULL_CALL} and
* {@link Connection#PROPERTY_IS_EXTERNAL_CALL}.
- * @hide
*/
public static final int CAPABILITY_CAN_PULL_CALL = 0x00800000;
@@ -305,10 +304,14 @@
* in its manifest.
* <p>
* See {@link Connection#PROPERTY_IS_EXTERNAL_CALL}.
- * @hide
*/
public static final int PROPERTY_IS_EXTERNAL_CALL = 0x00000040;
+ /**
+ * Indicates that the call has CDMA Enhanced Voice Privacy enabled.
+ */
+ public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 0x00000080;
+
//******************************************************************************************
// Next PROPERTY value: 0x00000100
//******************************************************************************************
@@ -465,6 +468,9 @@
if (hasProperty(properties, PROPERTY_IS_EXTERNAL_CALL)) {
builder.append(" PROPERTY_IS_EXTERNAL_CALL");
}
+ if(hasProperty(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) {
+ builder.append(" PROPERTY_HAS_CDMA_VOICE_PRIVACY");
+ }
builder.append("]");
return builder.toString();
}
@@ -786,7 +792,6 @@
* @param call The {@code Call} receiving the event.
* @param event The event.
* @param extras Extras associated with the connection event.
- * @hide
*/
public void onConnectionEvent(Call call, String event, Bundle extras) {}
}
@@ -965,7 +970,6 @@
* An {@link InCallService} will only see calls which support this method if it has the
* {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true}
* in its manifest.
- * @hide
*/
public void pullExternalCall() {
// If this isn't an external call, ignore the request.
@@ -988,7 +992,6 @@
*
* @param event The connection event.
* @param extras Bundle containing extra information associated with the event.
- * @hide
*/
public void sendCallEvent(String event, Bundle extras) {
mInCallAdapter.sendCallEvent(mTelecomCallId, event, extras);
@@ -1002,7 +1005,6 @@
* extras. Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
*
* @param extras The extras to add.
- * @hide
*/
public final void putExtras(Bundle extras) {
if (extras == null) {
@@ -1032,7 +1034,7 @@
}
/**
- * Adds an integer extra to this {@code Connection}.
+ * Adds an integer extra to this {@link Call}.
*
* @param key The extra key.
* @param value The value.
@@ -1047,7 +1049,7 @@
}
/**
- * Adds a string extra to this {@code Connection}.
+ * Adds a string extra to this {@link Call}.
*
* @param key The extra key.
* @param value The value.
@@ -1062,10 +1064,9 @@
}
/**
- * Removes extras from this {@code Connection}.
+ * Removes extras from this {@link Call}.
*
* @param keys The keys of the extras to remove.
- * @hide
*/
public final void removeExtras(List<String> keys) {
if (mExtras != null) {
@@ -1080,6 +1081,15 @@
}
/**
+ * Removes extras from this {@link Call}.
+ *
+ * @param keys The keys of the extras to remove.
+ */
+ public final void removeExtras(String ... keys) {
+ removeExtras(Arrays.asList(keys));
+ }
+
+ /**
* Obtains the parent of this {@code Call} in a conference, if any.
*
* @return The parent {@code Call}, or {@code null} if this {@code Call} is not a
diff --git a/telecomm/java/android/telecom/Conference.java b/telecomm/java/android/telecom/Conference.java
index 0227d27..0eabf15 100644
--- a/telecomm/java/android/telecom/Conference.java
+++ b/telecomm/java/android/telecom/Conference.java
@@ -24,6 +24,7 @@
import android.util.ArraySet;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -164,7 +165,6 @@
* {@link Connection} for valid values.
*
* @return A bitmask of the properties of the conference call.
- * @hide
*/
public final int getConnectionProperties() {
return mConnectionProperties;
@@ -396,7 +396,6 @@
* {@link Connection} for valid values.
*
* @param connectionProperties A bitmask of the {@code Properties} of the conference call.
- * @hide
*/
public final void setConnectionProperties(int connectionProperties) {
if (connectionProperties != mConnectionProperties) {
@@ -685,6 +684,8 @@
* Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
*
* @param extras The extras associated with this {@code Conference}.
+ * @deprecated Use {@link #putExtras(Bundle)} to add extras. Use {@link #removeExtras(List)}
+ * to remove extras.
*/
public final void setExtras(@Nullable Bundle extras) {
// Keeping putExtras and removeExtras in the same lock so that this operation happens as a
@@ -727,7 +728,6 @@
* Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
*
* @param extras The extras to add.
- * @hide
*/
public final void putExtras(@NonNull Bundle extras) {
if (extras == null) {
@@ -790,10 +790,9 @@
}
/**
- * Removes an extra from this {@link Conference}.
+ * Removes extras from this {@link Conference}.
*
- * @param keys The key of the extra key to remove.
- * @hide
+ * @param keys The keys of the extras to remove.
*/
public final void removeExtras(List<String> keys) {
if (keys == null || keys.isEmpty()) {
@@ -815,7 +814,25 @@
}
/**
+ * Removes extras from this {@link Conference}.
+ *
+ * @param keys The keys of the extras to remove.
+ */
+ public final void removeExtras(String ... keys) {
+ removeExtras(Arrays.asList(keys));
+ }
+
+ /**
* Returns the extras associated with this conference.
+ * <p>
+ * Extras should be updated using {@link #putExtras(Bundle)} and {@link #removeExtras(List)}.
+ * <p>
+ * Telecom or an {@link InCallService} can also update the extras via
+ * {@link android.telecom.Call#putExtras(Bundle)}, and
+ * {@link Call#removeExtras(List)}.
+ * <p>
+ * The conference is notified of changes to the extras made by Telecom or an
+ * {@link InCallService} by {@link #onExtrasChanged(Bundle)}.
*
* @return The extras associated with this connection.
*/
@@ -832,7 +849,6 @@
* {@link Call#removeExtras(List)}.
*
* @param extras The new extras bundle.
- * @hide
*/
public void onExtrasChanged(Bundle extras) {}
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index ff220f3..5843fbb 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -35,6 +35,7 @@
import android.view.Surface;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -100,7 +101,6 @@
* <p>
* A connection can only be in this state if the {@link #PROPERTY_IS_EXTERNAL_CALL} property and
* {@link #CAPABILITY_CAN_PULL_CALL} capability bits are set on the connection.
- * @hide
*/
public static final int STATE_PULLING_CALL = 7;
@@ -284,7 +284,6 @@
* <p>
* Should only be set on a {@code Connection} where {@link #PROPERTY_IS_EXTERNAL_CALL}
* is set.
- * @hide
*/
public static final int CAPABILITY_CAN_PULL_CALL = 0x01000000;
@@ -332,13 +331,16 @@
* external connections. Only those {@link InCallService}s which have the
* {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} in its
* manifest will see external connections.
- * @hide
*/
public static final int PROPERTY_IS_EXTERNAL_CALL = 1<<4;
+ /**
+ * Indicates that the connection has CDMA Enhanced Voice Privacy enabled.
+ */
+ public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 1<<5;
//**********************************************************************************************
- // Next PROPERTY value: 1<<5
+ // Next PROPERTY value: 1<<6
//**********************************************************************************************
/**
@@ -391,10 +393,15 @@
* {@link Call.Details#PROPERTY_IS_EXTERNAL_CALL} and
* {@link Call.Details#CAPABILITY_CAN_PULL_CALL}, but the {@link ConnectionService} could not
* pull the external call due to an error condition.
- * @hide
*/
public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED";
+ /**
+ * Connection event used to inform {@link InCallService}s when the merging of two calls has
+ * failed. The User Interface should use this message to inform the user of the error.
+ */
+ public static final String EVENT_CALL_MERGE_FAILED = "android.telecom.event.CALL_MERGE_FAILED";
+
// Flag controlling whether PII is emitted into the logs
private static final boolean PII_DEBUG = Log.isLoggable(android.util.Log.DEBUG);
@@ -510,13 +517,6 @@
return builder.toString();
}
- /**
- * Builds a string representation of a properties bit-mask.
- *
- * @param properties The properties bit-mask.
- * @return String representation.
- * @hide
- */
public static String propertiesToString(int properties) {
StringBuilder builder = new StringBuilder();
builder.append("[Properties:");
@@ -541,6 +541,10 @@
builder.append(" PROPERTY_IS_EXTERNAL_CALL");
}
+ if (can(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) {
+ builder.append(" PROPERTY_HAS_CDMA_VOICE_PRIVACY");
+ }
+
builder.append("]");
return builder.toString();
}
@@ -1385,6 +1389,15 @@
/**
* Returns the extras associated with this connection.
+ * <p>
+ * Extras should be updated using {@link #putExtras(Bundle)}.
+ * <p>
+ * Telecom or an {@link InCallService} can also update the extras via
+ * {@link android.telecom.Call#putExtras(Bundle)}, and
+ * {@link Call#removeExtras(List)}.
+ * <p>
+ * The connection is notified of changes to the extras made by Telecom or an
+ * {@link InCallService} by {@link #onExtrasChanged(Bundle)}.
*
* @return The extras associated with this connection.
*/
@@ -1493,7 +1506,6 @@
/**
* Returns the connection's properties, as a bit mask of the {@code PROPERTY_*} constants.
- * @hide
*/
public final int getConnectionProperties() {
return mConnectionProperties;
@@ -1699,7 +1711,6 @@
* Sets the connection's properties as a bit mask of the {@code PROPERTY_*} constants.
*
* @param connectionProperties The new connection properties.
- * @hide
*/
public final void setConnectionProperties(int connectionProperties) {
checkImmutable();
@@ -1884,6 +1895,8 @@
* Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
*
* @param extras The extras associated with this {@code Connection}.
+ * @deprecated Use {@link #putExtras(Bundle)} to add extras. Use {@link #removeExtras(List)}
+ * to remove extras.
*/
public final void setExtras(@Nullable Bundle extras) {
checkImmutable();
@@ -1924,7 +1937,6 @@
* Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
*
* @param extras The extras to add.
- * @hide
*/
public final void putExtras(@NonNull Bundle extras) {
checkImmutable();
@@ -1988,10 +2000,9 @@
}
/**
- * Removes an extra from this {@code Connection}.
+ * Removes extras from this {@code Connection}.
*
- * @param keys The key of the extra key to remove.
- * @hide
+ * @param keys The keys of the extras to remove.
*/
public final void removeExtras(List<String> keys) {
synchronized (mExtrasLock) {
@@ -2008,6 +2019,15 @@
}
/**
+ * Removes extras from this {@code Connection}.
+ *
+ * @param keys The keys of the extras to remove.
+ */
+ public final void removeExtras(String ... keys) {
+ removeExtras(Arrays.asList(keys));
+ }
+
+ /**
* Notifies this Connection that the {@link #getAudioState()} property has a new value.
*
* @param state The new connection audio state.
@@ -2129,7 +2149,6 @@
* capability and {@link Connection#PROPERTY_IS_EXTERNAL_CALL} property bits must be set.
* <p>
* For more information on external calls, see {@link Connection#PROPERTY_IS_EXTERNAL_CALL}.
- * @hide
*/
public void onPullExternalCall() {}
@@ -2142,7 +2161,6 @@
*
* @param event The call event.
* @param extras Extras associated with the call event.
- * @hide
*/
public void onCallEvent(String event, Bundle extras) {}
@@ -2155,7 +2173,6 @@
* {@link Call#removeExtras(List)}.
*
* @param extras The new extras bundle.
- * @hide
*/
public void onExtrasChanged(Bundle extras) {}
@@ -2340,7 +2357,6 @@
*
* @param event The connection event.
* @param extras Bundle containing extra information associated with the event.
- * @hide
*/
public void sendConnectionEvent(String event, Bundle extras) {
for (Listener l : mListeners) {
diff --git a/telecomm/java/android/telecom/DisconnectCause.java b/telecomm/java/android/telecom/DisconnectCause.java
index 65437d9..cf73d4f 100644
--- a/telecomm/java/android/telecom/DisconnectCause.java
+++ b/telecomm/java/android/telecom/DisconnectCause.java
@@ -67,13 +67,11 @@
/**
* Disconnected because the user did not locally answer the incoming call, but it was answered
* on another device where the call was ringing.
- * @hide
*/
public static final int ANSWERED_ELSEWHERE = 11;
/**
* Disconnected because the call was pulled from the current device to another device.
- * @hide
*/
public static final int CALL_PULLED = 12;
diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java
index e2399ff..df6715d 100644
--- a/telecomm/java/android/telecom/InCallService.java
+++ b/telecomm/java/android/telecom/InCallService.java
@@ -457,7 +457,6 @@
* @param call The call the event is associated with.
* @param event The event.
* @param extras Any associated extras.
- * @hide
*/
public void onConnectionEvent(Call call, String event, Bundle extras) {
}
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.java b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
index e7c9672..0ee9bab 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.java
+++ b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
@@ -20,11 +20,168 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* @hide
*/
@SystemApi
public class ParcelableCallAnalytics implements Parcelable {
+ public static final class AnalyticsEvent implements Parcelable {
+ public static final int SET_SELECT_PHONE_ACCOUNT = 0;
+ public static final int SET_ACTIVE = 1;
+ public static final int SET_DISCONNECTED = 2;
+ public static final int START_CONNECTION = 3;
+ public static final int SET_DIALING = 4;
+ public static final int BIND_CS = 5;
+ public static final int CS_BOUND = 6;
+ public static final int REQUEST_ACCEPT = 7;
+ public static final int REQUEST_REJECT = 8;
+
+ public static final int SCREENING_SENT = 100;
+ public static final int SCREENING_COMPLETED = 101;
+ public static final int DIRECT_TO_VM_INITIATED = 102;
+ public static final int DIRECT_TO_VM_FINISHED = 103;
+ public static final int BLOCK_CHECK_INITIATED = 104;
+ public static final int BLOCK_CHECK_FINISHED = 105;
+ public static final int FILTERING_INITIATED = 106;
+ public static final int FILTERING_COMPLETED = 107;
+ public static final int FILTERING_TIMED_OUT = 108;
+
+ public static final int SKIP_RINGING = 200;
+ public static final int SILENCE = 201;
+ public static final int MUTE = 202;
+ public static final int UNMUTE = 203;
+ public static final int AUDIO_ROUTE_BT = 204;
+ public static final int AUDIO_ROUTE_EARPIECE = 205;
+ public static final int AUDIO_ROUTE_HEADSET = 206;
+ public static final int AUDIO_ROUTE_SPEAKER = 207;
+
+ public static final int CONFERENCE_WITH = 300;
+ public static final int SPLIT_CONFERENCE = 301;
+ public static final int SET_PARENT = 302;
+
+ public static final int REQUEST_HOLD = 400;
+ public static final int REQUEST_UNHOLD = 401;
+ public static final int REMOTELY_HELD = 402;
+ public static final int REMOTELY_UNHELD = 403;
+ public static final int SET_HOLD = 404;
+ public static final int SWAP = 405;
+
+ public static final int REQUEST_PULL = 500;
+
+
+ public static final Parcelable.Creator<AnalyticsEvent> CREATOR =
+ new Parcelable.Creator<AnalyticsEvent> () {
+
+ @Override
+ public AnalyticsEvent createFromParcel(Parcel in) {
+ return new AnalyticsEvent(in);
+ }
+
+ @Override
+ public AnalyticsEvent[] newArray(int size) {
+ return new AnalyticsEvent[size];
+ }
+ };
+
+ private int mEventName;
+ private long mTimeSinceLastEvent;
+
+ public AnalyticsEvent(int eventName, long timestamp) {
+ mEventName = eventName;
+ mTimeSinceLastEvent = timestamp;
+ }
+
+ AnalyticsEvent(Parcel in) {
+ mEventName = in.readInt();
+ mTimeSinceLastEvent = in.readLong();
+ }
+
+ public int getEventName() {
+ return mEventName;
+ }
+
+ public long getTimeSinceLastEvent() {
+ return mTimeSinceLastEvent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mEventName);
+ out.writeLong(mTimeSinceLastEvent);
+ }
+ }
+
+ public static final class EventTiming implements Parcelable {
+ public static final int ACCEPT_TIMING = 0;
+ public static final int REJECT_TIMING = 1;
+ public static final int DISCONNECT_TIMING = 2;
+ public static final int HOLD_TIMING = 3;
+ public static final int UNHOLD_TIMING = 4;
+ public static final int OUTGOING_TIME_TO_DIALING_TIMING = 5;
+ public static final int BIND_CS_TIMING = 6;
+ public static final int SCREENING_COMPLETED_TIMING = 7;
+ public static final int DIRECT_TO_VM_FINISHED_TIMING = 8;
+ public static final int BLOCK_CHECK_FINISHED_TIMING = 9;
+ public static final int FILTERING_COMPLETED_TIMING = 10;
+ public static final int FILTERING_TIMED_OUT_TIMING = 11;
+
+ public static final int INVALID = 999999;
+
+ public static final Parcelable.Creator<EventTiming> CREATOR =
+ new Parcelable.Creator<EventTiming> () {
+
+ @Override
+ public EventTiming createFromParcel(Parcel in) {
+ return new EventTiming(in);
+ }
+
+ @Override
+ public EventTiming[] newArray(int size) {
+ return new EventTiming[size];
+ }
+ };
+
+ private int mName;
+ private long mTime;
+
+ public EventTiming(int name, long time) {
+ this.mName = name;
+ this.mTime = time;
+ }
+
+ private EventTiming(Parcel in) {
+ mName = in.readInt();
+ mTime = in.readLong();
+ }
+
+ public int getName() {
+ return mName;
+ }
+
+ public long getTime() {
+ return mTime;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mName);
+ out.writeLong(mTime);
+ }
+ }
+
public static final int CALLTYPE_UNKNOWN = 0;
public static final int CALLTYPE_INCOMING = 1;
public static final int CALLTYPE_OUTGOING = 2;
@@ -87,10 +244,17 @@
// Whether the call object was created from an existing connection.
private final boolean isCreatedFromExistingConnection;
+ // A list of events that are associated with this call
+ private final List<AnalyticsEvent> analyticsEvents;
+
+ // A map from event-pair names to their durations.
+ private final List<EventTiming> eventTimings;
+
public ParcelableCallAnalytics(long startTimeMillis, long callDurationMillis, int callType,
boolean isAdditionalCall, boolean isInterrupted, int callTechnologies,
int callTerminationCode, boolean isEmergencyCall, String connectionService,
- boolean isCreatedFromExistingConnection) {
+ boolean isCreatedFromExistingConnection, List<AnalyticsEvent> analyticsEvents,
+ List<EventTiming> eventTimings) {
this.startTimeMillis = startTimeMillis;
this.callDurationMillis = callDurationMillis;
this.callType = callType;
@@ -101,6 +265,8 @@
this.isEmergencyCall = isEmergencyCall;
this.connectionService = connectionService;
this.isCreatedFromExistingConnection = isCreatedFromExistingConnection;
+ this.analyticsEvents = analyticsEvents;
+ this.eventTimings = eventTimings;
}
public ParcelableCallAnalytics(Parcel in) {
@@ -114,6 +280,10 @@
isEmergencyCall = readByteAsBoolean(in);
connectionService = in.readString();
isCreatedFromExistingConnection = readByteAsBoolean(in);
+ analyticsEvents = new ArrayList<>();
+ in.readTypedList(analyticsEvents, AnalyticsEvent.CREATOR);
+ eventTimings = new ArrayList<>();
+ in.readTypedList(eventTimings, EventTiming.CREATOR);
}
public void writeToParcel(Parcel out, int flags) {
@@ -127,6 +297,8 @@
writeBooleanAsByte(out, isEmergencyCall);
out.writeString(connectionService);
writeBooleanAsByte(out, isCreatedFromExistingConnection);
+ out.writeTypedList(analyticsEvents);
+ out.writeTypedList(eventTimings);
}
public long getStartTimeMillis() {
@@ -169,6 +341,14 @@
return isCreatedFromExistingConnection;
}
+ public List<AnalyticsEvent> analyticsEvents() {
+ return analyticsEvents;
+ }
+
+ public List<EventTiming> getEventTimings() {
+ return eventTimings;
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/telecomm/java/android/telecom/RemoteConference.java b/telecomm/java/android/telecom/RemoteConference.java
index bf6038a..943da6d 100644
--- a/telecomm/java/android/telecom/RemoteConference.java
+++ b/telecomm/java/android/telecom/RemoteConference.java
@@ -97,7 +97,6 @@
*
* @param conference The {@code RemoteConference} invoking this method.
* @param connectionProperties The new properties of the {@code RemoteConference}.
- * @hide
*/
public void onConnectionPropertiesChanged(
RemoteConference conference,
diff --git a/telecomm/java/android/telecom/RemoteConnection.java b/telecomm/java/android/telecom/RemoteConnection.java
index 8e06659db..dc8eaf6 100644
--- a/telecomm/java/android/telecom/RemoteConnection.java
+++ b/telecomm/java/android/telecom/RemoteConnection.java
@@ -95,7 +95,6 @@
*
* @param connection The {@code RemoteConnection} invoking this method.
* @param connectionProperties The new properties of the {@code RemoteConnection}.
- * @hide
*/
public void onConnectionPropertiesChanged(
RemoteConnection connection,
@@ -230,7 +229,6 @@
* @param connection The {@code RemoteConnection} invoking this method.
* @param event The connection event.
* @param extras Extras associated with the event.
- * @hide
*/
public void onConnectionEvent(RemoteConnection connection, String event, Bundle extras) {}
}
@@ -738,7 +736,6 @@
*
* @return A bitmask of the properties of the {@code RemoteConnection}, as defined in the
* {@code PROPERTY_*} constants in class {@link Connection}.
- * @hide
*/
public int getConnectionProperties() {
return mConnectionProperties;
@@ -993,7 +990,6 @@
* Instructs this {@link RemoteConnection} to pull itself to the local device.
* <p>
* See {@link Call#pullExternalCall()} for more information.
- * @hide
*/
public void pullExternalCall() {
try {
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl b/telecomm/java/android/telecom/TelecomAnalytics.aidl
similarity index 94%
rename from telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
rename to telecomm/java/android/telecom/TelecomAnalytics.aidl
index b7e78d1..08ad0a2 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
+++ b/telecomm/java/android/telecom/TelecomAnalytics.aidl
@@ -19,4 +19,4 @@
/**
* {@hide}
*/
-parcelable ParcelableCallAnalytics;
+parcelable TelecomAnalytics;
diff --git a/telecomm/java/android/telecom/TelecomAnalytics.java b/telecomm/java/android/telecom/TelecomAnalytics.java
new file mode 100644
index 0000000..6e0d02c
--- /dev/null
+++ b/telecomm/java/android/telecom/TelecomAnalytics.java
@@ -0,0 +1,148 @@
+/*
+ * 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.telecom;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class TelecomAnalytics implements Parcelable {
+ public static final Parcelable.Creator<TelecomAnalytics> CREATOR =
+ new Parcelable.Creator<TelecomAnalytics> () {
+
+ @Override
+ public TelecomAnalytics createFromParcel(Parcel in) {
+ return new TelecomAnalytics(in);
+ }
+
+ @Override
+ public TelecomAnalytics[] newArray(int size) {
+ return new TelecomAnalytics[size];
+ }
+ };
+
+ public static final class SessionTiming extends TimedEvent<Integer> implements Parcelable {
+ public static final Parcelable.Creator<SessionTiming> CREATOR =
+ new Parcelable.Creator<SessionTiming> () {
+
+ @Override
+ public SessionTiming createFromParcel(Parcel in) {
+ return new SessionTiming(in);
+ }
+
+ @Override
+ public SessionTiming[] newArray(int size) {
+ return new SessionTiming[size];
+ }
+ };
+
+ public static final int ICA_ANSWER_CALL = 1;
+ public static final int ICA_REJECT_CALL = 2;
+ public static final int ICA_DISCONNECT_CALL = 3;
+ public static final int ICA_HOLD_CALL = 4;
+ public static final int ICA_UNHOLD_CALL = 5;
+ public static final int ICA_MUTE = 6;
+ public static final int ICA_SET_AUDIO_ROUTE = 7;
+ public static final int ICA_CONFERENCE = 8;
+
+ public static final int CSW_HANDLE_CREATE_CONNECTION_COMPLETE = 100;
+ public static final int CSW_SET_ACTIVE = 101;
+ public static final int CSW_SET_RINGING = 102;
+ public static final int CSW_SET_DIALING = 103;
+ public static final int CSW_SET_DISCONNECTED = 104;
+ public static final int CSW_SET_ON_HOLD = 105;
+ public static final int CSW_REMOVE_CALL = 106;
+ public static final int CSW_SET_IS_CONFERENCED = 107;
+ public static final int CSW_ADD_CONFERENCE_CALL = 108;
+
+ private int mId;
+ private long mTime;
+
+ public SessionTiming(int id, long time) {
+ this.mId = id;
+ this.mTime = time;
+ }
+
+ private SessionTiming(Parcel in) {
+ mId = in.readInt();
+ mTime = in.readLong();
+ }
+
+ @Override
+ public Integer getKey() {
+ return mId;
+ }
+
+ @Override
+ public long getTime() {
+ return mTime;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mId);
+ out.writeLong(mTime);
+ }
+ }
+
+ private List<SessionTiming> mSessionTimings;
+ private List<ParcelableCallAnalytics> mCallAnalytics;
+
+ public TelecomAnalytics(List<SessionTiming> sessionTimings,
+ List<ParcelableCallAnalytics> callAnalytics) {
+ this.mSessionTimings = sessionTimings;
+ this.mCallAnalytics = callAnalytics;
+ }
+
+ private TelecomAnalytics(Parcel in) {
+ mSessionTimings = new ArrayList<>();
+ in.readTypedList(mSessionTimings, SessionTiming.CREATOR);
+ mCallAnalytics = new ArrayList<>();
+ in.readTypedList(mCallAnalytics, ParcelableCallAnalytics.CREATOR);
+ }
+
+ public List<SessionTiming> getSessionTimings() {
+ return mSessionTimings;
+ }
+
+ public List<ParcelableCallAnalytics> getCallAnalytics() {
+ return mCallAnalytics;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeTypedList(mSessionTimings);
+ out.writeTypedList(mCallAnalytics);
+ }
+}
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index da0d048..f12886a 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -348,7 +348,6 @@
* informed of external calls should set this meta-data to {@code true} in the manifest
* registration of their {@link InCallService}. By default, the {@link InCallService} will NOT
* be informed of external calls.
- * @hide
*/
public static final String METADATA_INCLUDE_EXTERNAL_CALLS =
"android.telecom.INCLUDE_EXTERNAL_CALLS";
@@ -1444,9 +1443,9 @@
*/
@SystemApi
@RequiresPermission(Manifest.permission.DUMP)
- public List<ParcelableCallAnalytics> dumpAnalytics() {
+ public TelecomAnalytics dumpAnalytics() {
ITelecomService service = getTelecomService();
- List<ParcelableCallAnalytics> result = null;
+ TelecomAnalytics result = null;
if (service != null) {
try {
result = service.dumpCallAnalytics();
diff --git a/telecomm/java/android/telecom/TimedEvent.java b/telecomm/java/android/telecom/TimedEvent.java
new file mode 100644
index 0000000..e484e79
--- /dev/null
+++ b/telecomm/java/android/telecom/TimedEvent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.telecom;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+public abstract class TimedEvent<T> {
+ public abstract long getTime();
+ public abstract T getKey();
+
+ public static <T> Map<T, Double> averageTimings(Collection<? extends TimedEvent<T>> events) {
+ HashMap<T, Integer> counts = new HashMap<>();
+ HashMap<T, Double> result = new HashMap<>();
+
+ for (TimedEvent<T> entry : events) {
+ if (counts.containsKey(entry.getKey())) {
+ counts.put(entry.getKey(), counts.get(entry.getKey()) + 1);
+ result.put(entry.getKey(), result.get(entry.getKey()) + entry.getTime());
+ } else {
+ counts.put(entry.getKey(), 1);
+ result.put(entry.getKey(), (double) entry.getTime());
+ }
+ }
+
+ for (Map.Entry<T, Double> entry : result.entrySet()) {
+ result.put(entry.getKey(), entry.getValue() / counts.get(entry.getKey()));
+ }
+
+ return result;
+ }
+}
+
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index 871565d..5c412e7 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -18,7 +18,7 @@
import android.content.ComponentName;
import android.content.Intent;
-import android.telecom.ParcelableCallAnalytics;
+import android.telecom.TelecomAnalytics;
import android.telecom.PhoneAccountHandle;
import android.net.Uri;
import android.os.Bundle;
@@ -148,7 +148,7 @@
/**
* @see TelecomServiceImpl#dumpCallAnalytics
*/
- List<ParcelableCallAnalytics> dumpCallAnalytics();
+ TelecomAnalytics dumpCallAnalytics();
//
// Internal system apis relating to call management.
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index ff7ca62..44d89c1 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -530,6 +530,17 @@
public static final String KEY_SUPPORT_CONFERENCE_CALL_BOOL = "support_conference_call_bool";
/**
+ * Determines whether video conference calls are supported by a carrier. When {@code true},
+ * video calls can be merged into conference calls, {@code false} otherwiwse.
+ * <p>
+ * Note: even if video conference calls are not supported, audio calls may be merged into a
+ * conference if {@link #KEY_SUPPORT_CONFERENCE_CALL_BOOL} is {@code true}.
+ * @hide
+ */
+ public static final String KEY_SUPPORT_VIDEO_CONFERENCE_CALL_BOOL =
+ "support_video_conference_call_bool";
+
+ /**
* Determine whether user can toggle Enhanced 4G LTE Mode in Settings.
*/
public static final String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
@@ -582,6 +593,14 @@
public static final String KEY_WFC_DATA_SPN_FORMAT_IDX_INT = "wfc_data_spn_format_idx_int";
/**
+ * The Component Name of the activity that can setup the emergency addrees for WiFi Calling
+ * as per carrier requirement.
+ * @hide
+ */
+ public static final String KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING =
+ "wfc_emergency_address_carrier_app_string";
+
+ /**
* Boolean to decide whether to use #KEY_CARRIER_NAME_STRING from CarrierConfig app.
* @hide
*/
@@ -594,7 +613,6 @@
*/
public static final String KEY_CARRIER_NAME_STRING = "carrier_name_string";
-
/**
* If this is true, the SIM card (through Customer Service Profile EF file) will be able to
* prevent manual operator selection. If false, this SIM setting will be ignored and manual
@@ -615,6 +633,13 @@
public static final String KEY_BROADCAST_EMERGENCY_CALL_STATE_CHANGES_BOOL =
"broadcast_emergency_call_state_changes_bool";
+ /**
+ * Cell broadcast additional channels enbled by the carrier
+ * @hide
+ */
+ public static final String KEY_CARRIER_ADDITIONAL_CBS_CHANNELS_STRINGS =
+ "carrier_additional_cbs_channels_strings";
+
// These variables are used by the MMS service and exposed through another API, {@link
// SmsManager}. The variable names and string values are copied from there.
public static final String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
@@ -658,10 +683,44 @@
* example:
* <item>com.google.android.carrierPackageName</item>
* <item>com.google.android.carrierPackageName.CarrierActivityName</item>
+ * The ComponentName of the carrier activity that can setup the device and activate with the
+ * network as part of the Setup Wizard flow.
* @hide
*/
- public static final String KEY_SIM_PROVISIONING_STATUS_DETECTION_CARRIER_APP_STRING_ARRAY =
- "sim_state_detection_carrier_app_string_array";
+ public static final String KEY_CARRIER_SETUP_APP_STRING = "carrier_setup_app_string";
+
+ /**
+ * A list of component name of carrier signalling receivers which are interested in intent
+ * android.intent.action.CARRIER_SIGNAL_REDIRECTED.
+ * Example:
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameA</item>
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameB</item>
+ * @hide
+ */
+ public static final String KEY_SIGNAL_REDIRECTION_RECEIVER_STRING_ARRAY =
+ "signal_redirection_receiver_string_array";
+
+ /**
+ * A list of component name of carrier signalling receivers which are interested in intent
+ * android.intent.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED.
+ * Example:
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameA</item>
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameB</item>
+ * @hide
+ */
+ public static final String KEY_SIGNAL_DCFAILURE_RECEIVER_STRING_ARRAY =
+ "signal_dcfailure_receiver_string_array";
+
+ /**
+ * A list of component name of carrier signalling receivers which are interested in intent
+ * android.intent.action.CARRIER_SIGNAL_PCO_VALUE.
+ * Example:
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameA</item>
+ * <item>com.google.android.carrierPackageName/.CarrierSignalReceiverNameB</item>
+ * @hide
+ */
+ public static final String KEY_SIGNAL_PCO_RECEIVER_STRING_ARRAY =
+ "signal_pco_receiver_string_array";
/**
* Determines whether the carrier supports making non-emergency phone calls while the phone is
@@ -808,6 +867,7 @@
sDefaults.putInt(KEY_IMS_DTMF_TONE_DELAY_INT, 0);
sDefaults.putInt(KEY_CDMA_DTMF_TONE_DELAY_INT, 100);
sDefaults.putBoolean(KEY_SUPPORT_CONFERENCE_CALL_BOOL, true);
+ sDefaults.putBoolean(KEY_SUPPORT_VIDEO_CONFERENCE_CALL_BOOL, false);
sDefaults.putBoolean(KEY_EDITABLE_ENHANCED_4G_LTE_BOOL, true);
sDefaults.putBoolean(KEY_HIDE_IMS_APN_BOOL, false);
sDefaults.putBoolean(KEY_HIDE_PREFERRED_NETWORK_TYPE_BOOL, false);
@@ -816,6 +876,7 @@
sDefaults.putStringArray(KEY_WFC_OPERATOR_ERROR_CODES_STRING_ARRAY, null);
sDefaults.putInt(KEY_WFC_SPN_FORMAT_IDX_INT, 0);
sDefaults.putInt(KEY_WFC_DATA_SPN_FORMAT_IDX_INT, 0);
+ sDefaults.putString(KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING, "");
sDefaults.putBoolean(KEY_CONFIG_WIFI_DISABLE_IN_ECBM, false);
sDefaults.putBoolean(KEY_CARRIER_NAME_OVERRIDE_BOOL, false);
sDefaults.putString(KEY_CARRIER_NAME_STRING, "");
@@ -856,9 +917,11 @@
sDefaults.putBoolean(KEY_USE_RCS_PRESENCE_BOOL, false);
sDefaults.putInt(KEY_CDMA_ROAMING_MODE_INT, CDMA_ROAMING_MODE_RADIO_DEFAULT);
- // Used for Sim card State detection app
- sDefaults.putStringArray(KEY_SIM_PROVISIONING_STATUS_DETECTION_CARRIER_APP_STRING_ARRAY,
- null);
+ // Carrier Signalling Receivers
+ sDefaults.putStringArray(KEY_SIGNAL_REDIRECTION_RECEIVER_STRING_ARRAY, null);
+ sDefaults.putStringArray(KEY_SIGNAL_DCFAILURE_RECEIVER_STRING_ARRAY, null);
+ sDefaults.putStringArray(KEY_SIGNAL_PCO_RECEIVER_STRING_ARRAY, null);
+ sDefaults.putString(KEY_CARRIER_SETUP_APP_STRING, "");
}
/**
diff --git a/telephony/java/android/telephony/DisconnectCause.java b/telephony/java/android/telephony/DisconnectCause.java
index 9eb1304..7a9170f 100644
--- a/telephony/java/android/telephony/DisconnectCause.java
+++ b/telephony/java/android/telephony/DisconnectCause.java
@@ -194,6 +194,12 @@
*/
public static final int VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED = 50;
+ /**
+ * The call was terminated because it was pulled to another device.
+ * {@hide}
+ */
+ public static final int CALL_PULLED = 51;
+
//*********************************************************************************************
// When adding a disconnect type:
// 1) Please assign the new type the next id value below.
@@ -318,7 +324,9 @@
case CDMA_ALREADY_ACTIVATED:
return "CDMA_ALREADY_ACTIVATED";
case VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED:
- return "VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED";
+ return "VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED";
+ case CALL_PULLED:
+ return "CALL_PULLED";
default:
return "INVALID: " + cause;
}
diff --git a/telephony/java/android/telephony/PcoData.aidl b/telephony/java/android/telephony/PcoData.aidl
new file mode 100644
index 0000000..a93b8d3
--- /dev/null
+++ b/telephony/java/android/telephony/PcoData.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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.telephony;
+
+parcelable PcoData;
+
diff --git a/telephony/java/android/telephony/PcoData.java b/telephony/java/android/telephony/PcoData.java
new file mode 100644
index 0000000..3e735e7
--- /dev/null
+++ b/telephony/java/android/telephony/PcoData.java
@@ -0,0 +1,87 @@
+/*
+ * 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.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Contains Carrier-specific (and opaque) Protocol configuration Option
+ * Data. In general this is only passed on to carrier-specific applications
+ * for interpretation.
+ *
+ * @hide
+ */
+public class PcoData implements Parcelable {
+
+ public final int cid;
+ public final String bearerProto;
+ public final int pcoId;
+ public final byte[] contents;
+
+ public PcoData(int cid, String bearerProto, int pcoId, byte[]contents) {
+ this.cid = cid;
+ this.bearerProto = bearerProto;
+ this.pcoId = pcoId;
+ this.contents = contents;
+ }
+
+ public PcoData(Parcel in) {
+ cid = in.readInt();
+ bearerProto = in.readString();
+ pcoId = in.readInt();
+ contents = in.createByteArray();
+ }
+
+ /**
+ * {@link Parcelable#writeToParcel}
+ */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(cid);
+ out.writeString(bearerProto);
+ out.writeInt(pcoId);
+ out.writeByteArray(contents);
+ }
+
+ /**
+ * {@link Parcelable#describeContents}
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@link Parcelable.Creator}
+ *
+ * @hide
+ */
+ public static final Parcelable.Creator<PcoData> CREATOR = new Parcelable.Creator() {
+ public PcoData createFromParcel(Parcel in) {
+ return new PcoData(in);
+ }
+
+ public PcoData[] newArray(int size) {
+ return new PcoData[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + ", contents[" +
+ contents.length + "])";
+ }
+}
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index 3c30251..6b2ae3e 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -30,6 +30,7 @@
import android.os.SystemProperties;
import android.provider.Contacts;
import android.provider.ContactsContract;
+import android.telecom.PhoneAccount;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
@@ -2599,6 +2600,48 @@
}
/**
+ * Given a {@link Uri} with a {@code sip} scheme, attempts to build an equivalent {@code tel}
+ * scheme {@link Uri}. If the source {@link Uri} does not contain a valid number, or is not
+ * using the {@code sip} scheme, the original {@link Uri} is returned.
+ *
+ * @param source The {@link Uri} to convert.
+ * @return The equivalent {@code tel} scheme {@link Uri}.
+ *
+ * @hide
+ */
+ public static Uri convertSipUriToTelUri(Uri source) {
+ // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
+ // Per RFC3261, the "user" can be a telephone number.
+ // For example: sip:1650555121;phone-context=blah.com@host.com
+ // In this case, the phone number is in the user field of the URI, and the parameters can be
+ // ignored.
+ //
+ // A SIP URI can also specify a phone number in a format similar to:
+ // sip:+1-212-555-1212@something.com;user=phone
+ // In this case, the phone number is again in user field and the parameters can be ignored.
+ // We can get the user field in these instances by splitting the string on the @, ;, or :
+ // and looking at the first found item.
+
+ String scheme = source.getScheme();
+
+ if (!PhoneAccount.SCHEME_SIP.equals(scheme)) {
+ // Not a sip URI, bail.
+ return source;
+ }
+
+ String number = source.getSchemeSpecificPart();
+ String numberParts[] = number.split("[@;:]");
+
+ if (numberParts.length == 0) {
+ // Number not found, bail.
+ return source;
+ }
+ number = numberParts[0];
+
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
+ }
+
+ /**
* This function handles the plus code conversion
* If the number format is
* 1)+1NANP,remove +,
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index b5cf212..6229ed9 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -90,14 +90,6 @@
private int mDataRoaming;
/**
- * Sim Provisioning Status:
- * {@See SubscriptionManager#SIM_PROVISIONED}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_COLD}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_OUT_OF_CREDIT}
- */
- private int mSimProvisioningStatus;
-
- /**
* SIM Icon bitmap
*/
private Bitmap mIconBitmap;
@@ -122,7 +114,7 @@
*/
public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
- Bitmap icon, int mcc, int mnc, String countryIso, int simProvisioningStatus) {
+ Bitmap icon, int mcc, int mnc, String countryIso) {
this.mId = id;
this.mIccId = iccId;
this.mSimSlotIndex = simSlotIndex;
@@ -136,7 +128,6 @@
this.mMcc = mcc;
this.mMnc = mnc;
this.mCountryIso = countryIso;
- this.mSimProvisioningStatus = simProvisioningStatus;
}
/**
@@ -273,17 +264,6 @@
}
/**
- * @return Sim Provisioning Status
- * {@See SubscriptionManager#SIM_PROVISIONED}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_COLD}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_OUT_OF_CREDIT}
- * @hide
- */
- public int getSimProvisioningStatus() {
- return this.mSimProvisioningStatus;
- }
-
- /**
* @return the MCC.
*/
public int getMcc() {
@@ -319,12 +299,10 @@
int mcc = source.readInt();
int mnc = source.readInt();
String countryIso = source.readString();
- int simProvisioningStatus = source.readInt();
Bitmap iconBitmap = Bitmap.CREATOR.createFromParcel(source);
return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName,
- nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso,
- simProvisioningStatus);
+ nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso);
}
@Override
@@ -347,7 +325,6 @@
dest.writeInt(mMcc);
dest.writeInt(mMnc);
dest.writeString(mCountryIso);
- dest.writeInt(mSimProvisioningStatus);
mIconBitmap.writeToParcel(dest, flags);
}
@@ -378,6 +355,6 @@
+ " displayName=" + mDisplayName + " carrierName=" + mCarrierName
+ " nameSource=" + mNameSource + " iconTint=" + mIconTint
+ " dataRoaming=" + mDataRoaming + " iconBitmap=" + mIconBitmap + " mcc " + mMcc
- + " mnc " + mMnc + " SimProvisioningStatus " + mSimProvisioningStatus +"}";
+ + " mnc " + mMnc + "}";
}
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index c49966a..dd6f9cb 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -232,25 +232,12 @@
/** Indicates that data roaming is disabled for a subscription */
public static final int DATA_ROAMING_DISABLE = 0;
- /** Sim provisioning status: provisioned */
- /** @hide */
- public static final int SIM_PROVISIONED = 0;
-
- /** Sim provisioning status: un-provisioned due to cold sim */
- /** @hide */
- public static final int SIM_UNPROVISIONED_COLD = 1;
-
- /** Sim provisioning status: un-provisioned due to out of credit */
- /** @hide */
- public static final int SIM_UNPROVISIONED_OUT_OF_CREDIT = 2;
-
- /** Maximum possible sim provisioning status */
- /** @hide */
- public static final int MAX_SIM_PROVISIONING_STATUS = SIM_UNPROVISIONED_OUT_OF_CREDIT;
-
/** @hide */
public static final int DATA_ROAMING_DEFAULT = DATA_ROAMING_DISABLE;
+ /** @hide */
+ public static final int SIM_PROVISIONED = 0;
+
/**
* TelephonyProvider column name for the MCC associated with a SIM.
* <P>Type: INTEGER (int)</P>
@@ -843,40 +830,6 @@
}
/**
- * Set Sim Provisioning Status by subscription ID
- * @param simProvisioningStatus with the subscription
- * {@See SubscriptionManager#SIM_PROVISIONED}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_COLD}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_OUT_OF_CREDIT}
- * @param subId the unique SubInfoRecord index in database
- * @return the number of records updated
- * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
- * @hide
- */
- public int setSimProvisioningStatus(int simProvisioningStatus, int subId) {
- if (VDBG) {
- logd("[setSimProvisioningStatus]+ status:" + simProvisioningStatus + " subId:" + subId);
- }
- if (simProvisioningStatus < 0 || simProvisioningStatus > MAX_SIM_PROVISIONING_STATUS ||
- !isValidSubscriptionId(subId)) {
- logd("[setSimProvisioningStatus]- fail");
- return -1;
- }
-
- int result = 0;
-
- try {
- ISub iSub = ISub.Stub.asInterface(ServiceManager.getService("isub"));
- if (iSub != null) {
- result = iSub.setSimProvisioningStatus(simProvisioningStatus, subId);
- }
- } catch (RemoteException ex) {
- // ignore it
- }
- return result;
- }
-
- /**
* Get slotId associated with the subscription.
* @return slotId as a positive integer or a negative value if an error either
* SIM_NOT_INSERTED or < 0 if an invalid slot index
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl b/telephony/java/android/telephony/TelephonyHistogram.aidl
similarity index 74%
copy from telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
copy to telephony/java/android/telephony/TelephonyHistogram.aidl
index b7e78d1..8de81cf 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.aidl
+++ b/telephony/java/android/telephony/TelephonyHistogram.aidl
@@ -1,11 +1,11 @@
/*
- * Copyright 2016, The Android Open Source Project
+ * 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
+ * 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,
@@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package android.telecom;
-
+package android.telephony;
/**
- * {@hide}
+ * @hide
*/
-parcelable ParcelableCallAnalytics;
+parcelable TelephonyHistogram;
diff --git a/telephony/java/android/telephony/TelephonyHistogram.java b/telephony/java/android/telephony/TelephonyHistogram.java
new file mode 100644
index 0000000..e1c3d7b
--- /dev/null
+++ b/telephony/java/android/telephony/TelephonyHistogram.java
@@ -0,0 +1,313 @@
+/*
+ * 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.telephony;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parcelable class to store Telephony histogram.
+ * @hide
+ */
+@SystemApi
+public final class TelephonyHistogram implements Parcelable {
+ // Type of Telephony histogram Eg: RIL histogram will have all timing data associated with
+ // RIL calls. Similarly we can have any other Telephony histogram.
+ private final int mCategory;
+
+ // Unique Id identifying a sample within particular category of histogram
+ private final int mId;
+
+ // Min time taken in ms
+ private int mMinTimeMs;
+
+ // Max time taken in ms
+ private int mMaxTimeMs;
+
+ // Average time taken in ms
+ private int mAverageTimeMs;
+
+ // Total count of samples
+ private int mSampleCount;
+
+ // Array storing time taken for first #RANGE_CALCULATION_COUNT samples of histogram.
+ private int[] mInitialTimings;
+
+ // Total number of time ranges expected (must be greater than 1)
+ private final int mBucketCount;
+
+ // Array storing endpoints of range buckets. Calculated based on values of minTime & maxTime
+ // after totalTimeCount is #RANGE_CALCULATION_COUNT.
+ private final int[] mBucketEndPoints;
+
+ // Array storing counts for each time range starting from smallest value range
+ private final int[] mBucketCounters;
+
+ /**
+ * Constant for Telephony category
+ */
+ public static final int TELEPHONY_CATEGORY_RIL = 1;
+
+ // Count of Histogram samples after which time buckets are created.
+ private static final int RANGE_CALCULATION_COUNT = 10;
+
+
+ // Constant used to indicate #initialTimings is null while parceling
+ private static final int ABSENT = 0;
+
+ // Constant used to indicate #initialTimings is not null while parceling
+ private static final int PRESENT = 1;
+
+ // Throws exception if #totalBuckets is not greater than one.
+ public TelephonyHistogram (int category, int id, int bucketCount) {
+ if (bucketCount <= 1) {
+ throw new IllegalArgumentException("Invalid number of buckets");
+ }
+ mCategory = category;
+ mId = id;
+ mMinTimeMs = Integer.MAX_VALUE;
+ mMaxTimeMs = 0;
+ mAverageTimeMs = 0;
+ mSampleCount = 0;
+ mInitialTimings = new int[RANGE_CALCULATION_COUNT];
+ mBucketCount = bucketCount;
+ mBucketEndPoints = new int[bucketCount - 1];
+ mBucketCounters = new int[bucketCount];
+ }
+
+ public TelephonyHistogram(TelephonyHistogram th) {
+ mCategory = th.getCategory();
+ mId = th.getId();
+ mMinTimeMs = th.getMinTime();
+ mMaxTimeMs = th.getMaxTime();
+ mAverageTimeMs = th.getAverageTime();
+ mSampleCount = th.getSampleCount();
+ mInitialTimings = th.getInitialTimings();
+ mBucketCount = th.getBucketCount();
+ mBucketEndPoints = th.getBucketEndPoints();
+ mBucketCounters = th.getBucketCounters();
+ }
+
+ public int getCategory() {
+ return mCategory;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getMinTime() {
+ return mMinTimeMs;
+ }
+
+ public int getMaxTime() {
+ return mMaxTimeMs;
+ }
+
+ public int getAverageTime() {
+ return mAverageTimeMs;
+ }
+
+ public int getSampleCount () {
+ return mSampleCount;
+ }
+
+ private int[] getInitialTimings() {
+ return mInitialTimings;
+ }
+
+ public int getBucketCount() {
+ return mBucketCount;
+ }
+
+ public int[] getBucketEndPoints() {
+ if (mSampleCount > 1 && mSampleCount < 10) {
+ int[] tempEndPoints = new int[mBucketCount - 1];
+ calculateBucketEndPoints(tempEndPoints);
+ return tempEndPoints;
+ } else {
+ return getDeepCopyOfArray(mBucketEndPoints);
+ }
+ }
+
+ public int[] getBucketCounters() {
+ if (mSampleCount > 1 && mSampleCount < 10) {
+ int[] tempEndPoints = new int[mBucketCount - 1];
+ int[] tempBucketCounters = new int[mBucketCount];
+ calculateBucketEndPoints(tempEndPoints);
+ for (int j = 0; j < mSampleCount; j++) {
+ addToBucketCounter(tempEndPoints, tempBucketCounters, mInitialTimings[j]);
+ }
+ return tempBucketCounters;
+ } else {
+ return getDeepCopyOfArray(mBucketCounters);
+ }
+ }
+
+ private int[] getDeepCopyOfArray(int[] array) {
+ int[] clone = new int[array.length];
+ System.arraycopy(array, 0, clone, 0, array.length);
+ return clone;
+ }
+
+ private void addToBucketCounter(int[] bucketEndPoints, int[] bucketCounters, int time) {
+ int i;
+ for (i = 0; i < bucketEndPoints.length; i++) {
+ if (time <= bucketEndPoints[i]) {
+ bucketCounters[i]++;
+ return;
+ }
+ }
+ bucketCounters[i]++;
+ }
+
+ private void calculateBucketEndPoints(int[] bucketEndPoints) {
+ for (int i = 1; i < mBucketCount; i++) {
+ int endPt = mMinTimeMs + (i * (mMaxTimeMs - mMinTimeMs)) / mBucketCount;
+ bucketEndPoints[i - 1] = endPt;
+ }
+ }
+
+ // Add new value of time taken
+ // This function updates minTime, maxTime, averageTime & totalTimeCount every time it is
+ // called. initialTimings[] is updated if totalTimeCount <= #RANGE_CALCULATION_COUNT. When
+ // totalTimeCount = RANGE_CALCULATION_COUNT, based on the min, max time & the number of buckets
+ // expected, bucketEndPoints[] would be calculated. Then bucketCounters[] would be filled up
+ // using values stored in initialTimings[]. Thereafter bucketCounters[] will always be updated.
+ public void addTimeTaken(int time) {
+ // Initialize all fields if its first entry or if integer overflow is going to occur while
+ // trying to calculate averageTime
+ if (mSampleCount == 0 || (mSampleCount == Integer.MAX_VALUE)) {
+ if (mSampleCount == 0) {
+ mMinTimeMs = time;
+ mMaxTimeMs = time;
+ mAverageTimeMs = time;
+ } else {
+ mInitialTimings = new int[RANGE_CALCULATION_COUNT];
+ }
+ mSampleCount = 1;
+ Arrays.fill(mInitialTimings, 0);
+ mInitialTimings[0] = time;
+ Arrays.fill(mBucketEndPoints, 0);
+ Arrays.fill(mBucketCounters, 0);
+ } else {
+ if (time < mMinTimeMs) {
+ mMinTimeMs = time;
+ }
+ if (time > mMaxTimeMs) {
+ mMaxTimeMs = time;
+ }
+ long totalTime = ((long)mAverageTimeMs) * mSampleCount + time;
+ mAverageTimeMs = (int)(totalTime/++mSampleCount);
+
+ if (mSampleCount < RANGE_CALCULATION_COUNT) {
+ mInitialTimings[mSampleCount - 1] = time;
+ } else if (mSampleCount == RANGE_CALCULATION_COUNT) {
+ mInitialTimings[mSampleCount - 1] = time;
+
+ // Calculate bucket endpoints based on bucketCount expected
+ calculateBucketEndPoints(mBucketEndPoints);
+
+ // Use values stored in initialTimings[] to update bucketCounters
+ for (int j = 0; j < RANGE_CALCULATION_COUNT; j++) {
+ addToBucketCounter(mBucketEndPoints, mBucketCounters, mInitialTimings[j]);
+ }
+ mInitialTimings = null;
+ } else {
+ addToBucketCounter(mBucketEndPoints, mBucketCounters, time);
+ }
+
+ }
+ }
+
+ public String toString() {
+ String basic = " Histogram id = " + mId + " Time(ms): min = " + mMinTimeMs + " max = "
+ + mMaxTimeMs + " avg = " + mAverageTimeMs + " Count = " + mSampleCount;
+ if (mSampleCount < RANGE_CALCULATION_COUNT) {
+ return basic;
+ } else {
+ StringBuffer intervals = new StringBuffer(" Interval Endpoints:");
+ for (int i = 0; i < mBucketEndPoints.length; i++) {
+ intervals.append(" " + mBucketEndPoints[i]);
+ }
+ intervals.append(" Interval counters:");
+ for (int i = 0; i < mBucketCounters.length; i++) {
+ intervals.append(" " + mBucketCounters[i]);
+ }
+ return basic + intervals;
+ }
+ }
+
+ public static final Parcelable.Creator<TelephonyHistogram> CREATOR =
+ new Parcelable.Creator<TelephonyHistogram> () {
+
+ @Override
+ public TelephonyHistogram createFromParcel(Parcel in) {
+ return new TelephonyHistogram(in);
+ }
+
+ @Override
+ public TelephonyHistogram[] newArray(int size) {
+ return new TelephonyHistogram[size];
+ }
+ };
+
+ public TelephonyHistogram(Parcel in) {
+ mCategory = in.readInt();
+ mId = in.readInt();
+ mMinTimeMs = in.readInt();
+ mMaxTimeMs = in.readInt();
+ mAverageTimeMs = in.readInt();
+ mSampleCount = in.readInt();
+ if (in.readInt() == PRESENT) {
+ mInitialTimings = new int[RANGE_CALCULATION_COUNT];
+ in.readIntArray(mInitialTimings);
+ }
+ mBucketCount = in.readInt();
+ mBucketEndPoints = new int[mBucketCount - 1];
+ in.readIntArray(mBucketEndPoints);
+ mBucketCounters = new int[mBucketCount];
+ in.readIntArray(mBucketCounters);
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mCategory);
+ out.writeInt(mId);
+ out.writeInt(mMinTimeMs);
+ out.writeInt(mMaxTimeMs);
+ out.writeInt(mAverageTimeMs);
+ out.writeInt(mSampleCount);
+ if (mInitialTimings == null) {
+ out.writeInt(ABSENT);
+ } else {
+ out.writeInt(PRESENT);
+ out.writeIntArray(mInitialTimings);
+ }
+ out.writeInt(mBucketCount);
+ out.writeIntArray(mBucketEndPoints);
+ out.writeIntArray(mBucketCounters);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0875ff9..78e15bf 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -34,8 +34,10 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
+import android.service.carrier.CarrierIdentifier;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyHistogram;
import android.util.Log;
import com.android.internal.telecom.ITelecomService;
@@ -50,6 +52,7 @@
import java.io.FileInputStream;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
@@ -1756,6 +1759,11 @@
*@hide
*/
public static final int SIM_STATE_CARD_IO_ERROR = 8;
+ /** SIM card state: SIM Card restricted, present but not usable due to
+ * carrier restrictions.
+ *@hide
+ */
+ public static final int SIM_STATE_CARD_RESTRICTED = 9;
/**
* @return true if a ICC card is present
@@ -1909,7 +1917,7 @@
return getSimOperatorNumericForPhone(phoneId);
}
- /**
+ /**
* Returns the MCC+MNC (mobile country code + mobile network code) of the
* provider of the SIM for a particular subscription. 5 or 6 decimal digits.
* <p>
@@ -2461,6 +2469,102 @@
}
/**
+ * Enables the visual voicemail SMS filter for a phone account. When the filter is
+ * enabled, Incoming SMS messages matching the OMTP VVM SMS interface will be redirected to the
+ * visual voicemail client with
+ * {@link android.provider.VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED}.
+ *
+ * <p>This takes effect only when the caller is the default dialer. The enabled status and
+ * settings persist through default dialer changes, but the filter will only honor the setting
+ * set by the current default dialer.
+ *
+ *
+ * @param subId The subscription id of the phone account.
+ * @param settings The settings for the filter.
+ */
+ /** @hide */
+ public void enableVisualVoicemailSmsFilter(int subId,
+ VisualVoicemailSmsFilterSettings settings) {
+ if(settings == null){
+ throw new IllegalArgumentException("Settings cannot be null");
+ }
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ telephony.enableVisualVoicemailSmsFilter(mContext.getOpPackageName(), subId,
+ settings);
+ }
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+ }
+
+ /**
+ * Disables the visual voicemail SMS filter for a phone account.
+ *
+ * <p>This takes effect only when the caller is the default dialer. The enabled status and
+ * settings persist through default dialer changes, but the filter will only honor the setting
+ * set by the current default dialer.
+ */
+ /** @hide */
+ public void disableVisualVoicemailSmsFilter(int subId) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ telephony.disableVisualVoicemailSmsFilter(mContext.getOpPackageName(), subId);
+ }
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+ }
+
+ /**
+ * @returns the settings of the visual voicemail SMS filter for a phone account, or {@code null}
+ * if the filter is disabled.
+ *
+ * <p>This takes effect only when the caller is the default dialer. The enabled status and
+ * settings persist through default dialer changes, but the filter will only honor the setting
+ * set by the current default dialer.
+ */
+ /** @hide */
+ @Nullable
+ public VisualVoicemailSmsFilterSettings getVisualVoicemailSmsFilterSettings(int subId) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony
+ .getVisualVoicemailSmsFilterSettings(mContext.getOpPackageName(), subId);
+ }
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+
+ return null;
+ }
+
+ /**
+ * @returns the settings of the visual voicemail SMS filter for a phone account set by the
+ * package, or {@code null} if the filter is disabled.
+ *
+ * <p>Requires the calling app to have READ_PRIVILEGED_PHONE_STATE permission.
+ */
+ /** @hide */
+ @Nullable
+ public VisualVoicemailSmsFilterSettings getVisualVoicemailSmsFilterSettings(String packageName,
+ int subId) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony.getSystemVisualVoicemailSmsFilterSettings(packageName, subId);
+ }
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+
+ return null;
+ }
+
+ /**
* Returns the voice mail count. Return 0 if unavailable, -1 if there are unread voice messages
* but the count is unknown.
* <p>
@@ -5246,4 +5350,170 @@
}
return false;
}
+
+ /**
+ * Return the application ID for the app type like {@link APPTYPE_CSIM}.
+ *
+ * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
+ *
+ * @param appType the uicc app type like {@link APPTYPE_CSIM}
+ * @return Application ID for specificied app type or null if no uicc or error.
+ * @hide
+ */
+ public String getAidForAppType(int appType) {
+ return getAidForAppType(getDefaultSubscription(), appType);
+ }
+
+ /**
+ * Return the application ID for the app type like {@link APPTYPE_CSIM}.
+ *
+ * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
+ *
+ * @param subId the subscription ID that this request applies to.
+ * @param appType the uicc app type, like {@link APPTYPE_CSIM}
+ * @return Application ID for specificied app type or null if no uicc or error.
+ * @hide
+ */
+ public String getAidForAppType(int subId, int appType) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ return service.getAidForAppType(subId, appType);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#getAidForAppType", e);
+ }
+ return null;
+ }
+
+ /**
+ * Return the Electronic Serial Number.
+ *
+ * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
+ *
+ * @return ESN or null if error.
+ * @hide
+ */
+ public String getEsn() {
+ return getEsn(getDefaultSubscription());
+ }
+
+ /**
+ * Return the Electronic Serial Number.
+ *
+ * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
+ *
+ * @param subId the subscription ID that this request applies to.
+ * @return ESN or null if error.
+ * @hide
+ */
+ public String getEsn(int subId) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ return service.getEsn(subId);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#getEsn", e);
+ }
+ return null;
+ }
+
+ /*
+ * Get snapshot of Telephony histograms
+ * @return List of Telephony histograms
+ * Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ * @hide
+ */
+ @SystemApi
+ public List<TelephonyHistogram> getTelephonyHistograms() {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ return service.getTelephonyHistograms();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#getTelephonyHistograms", e);
+ }
+ return null;
+ }
+
+ /**
+ * Set the allowed carrier list for slotId
+ * Require system privileges. In the future we may add this to carrier APIs.
+ *
+ * @return The number of carriers set successfully. Should be length of
+ * carrierList on success; -1 on error.
+ * @hide
+ */
+ public int setAllowedCarriers(int slotId, List<CarrierIdentifier> carriers) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ return service.setAllowedCarriers(slotId, carriers);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#setAllowedCarriers", e);
+ }
+ return -1;
+ }
+
+ /**
+ * Get the allowed carrier list for slotId.
+ * Require system privileges. In the future we may add this to carrier APIs.
+ *
+ * @return List of {@link android.telephony.CarrierIdentifier}; empty list
+ * means all carriers are allowed.
+ * @hide
+ */
+ public List<CarrierIdentifier> getAllowedCarriers(int slotId) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ return service.getAllowedCarriers(slotId);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#getAllowedCarriers", e);
+ }
+ return new ArrayList<CarrierIdentifier>(0);
+ }
+
+ /**
+ * Action set from carrier signalling broadcast receivers to enable/disable metered apns
+ * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
+ * @param subId the subscription ID that this action applies to.
+ * @param enabled control enable or disable metered apns.
+ * @hide
+ */
+ public void carrierActionSetMeteredApnsEnabled(int subId, boolean enabled) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ service.carrierActionSetMeteredApnsEnabled(subId, enabled);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#carrierActionSetMeteredApnsEnabled", e);
+ }
+ }
+
+ /**
+ * Action set from carrier signalling broadcast receivers to enable/disable radio
+ * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
+ * @param subId the subscription ID that this action applies to.
+ * @param enabled control enable or disable radio.
+ * @hide
+ */
+ public void carrierActionSetRadioEnabled(int subId, boolean enabled) {
+ try {
+ ITelephony service = getITelephony();
+ if (service != null) {
+ service.carrierActionSetRadioEnabled(subId, enabled);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#carrierActionSetRadioEnabled", e);
+ }
+ }
}
+
diff --git a/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.aidl b/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.aidl
new file mode 100644
index 0000000..4b0539d
--- /dev/null
+++ b/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.aidl
@@ -0,0 +1,19 @@
+/*
+* 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.telephony;
+
+parcelable VisualVoicemailSmsFilterSettings;
diff --git a/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.java b/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.java
new file mode 100644
index 0000000..5b81027
--- /dev/null
+++ b/telephony/java/android/telephony/VisualVoicemailSmsFilterSettings.java
@@ -0,0 +1,174 @@
+/*
+ * 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.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class to represent various settings for the visual voicemail SMS filter. When the filter is
+ * enabled, incoming SMS matching the generalized OMTP format:
+ *
+ * <p>[clientPrefix]:[prefix]:([key]=[value];)*
+ *
+ * <p>will be regarded as a visual voicemail SMS, and removed before reaching the SMS provider. The
+ * intent {@link android.provider.VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will then be sent
+ * to the default dialer with the information extracted from the SMS.
+ *
+ * <p>Use {@link android.telephony.VisualVoicemailSmsFilterSettings.Builder} to construct this
+ * class.
+ *
+ * @see android.telephony.TelephonyManager#enableVisualVoicemailSmsFilter
+ *
+ * @hide
+ */
+public class VisualVoicemailSmsFilterSettings implements Parcelable {
+
+
+ /**
+ * The visual voicemail SMS message does not have to be a data SMS, and can be directed to any
+ * port.
+ *
+ * @hide
+ */
+ public static final int DESTINATION_PORT_ANY = -1;
+
+ /**
+ * The visual voicemail SMS message can be directed to any port, but must be a data SMS.
+ *
+ * @hide
+ */
+ public static final int DESTINATION_PORT_DATA_SMS = -2;
+
+ public static final String DEFAULT_CLIENT_PREFIX = "//VVM";
+ public static final List<String> DEFAULT_ORIGINATING_NUMBERS = Collections.emptyList();
+ public static final int DEFAULT_DESTINATION_PORT = DESTINATION_PORT_ANY;
+
+ /**
+ * Builder class for {@link VisualVoicemailSmsFilterSettings} objects.
+ *
+ * @hide
+ */
+ public static class Builder {
+
+ private String mClientPrefix = DEFAULT_CLIENT_PREFIX;
+ private List<String> mOriginatingNumbers = DEFAULT_ORIGINATING_NUMBERS;
+ private int mDestinationPort = DEFAULT_DESTINATION_PORT;
+
+ public VisualVoicemailSmsFilterSettings build() {
+ return new VisualVoicemailSmsFilterSettings(this);
+ }
+
+ /**
+ * Sets the client prefix for the visual voicemail SMS filter. The client prefix will appear
+ * at the start of a visual voicemail SMS message, followed by a colon(:).
+ */
+ public Builder setClientPrefix(String clientPrefix) {
+ if (clientPrefix == null) {
+ throw new IllegalArgumentException("Client prefix cannot be null");
+ }
+ mClientPrefix = clientPrefix;
+ return this;
+ }
+
+ /**
+ * Sets the originating number whitelist for the visual voicemail SMS filter. If the list is
+ * not null only the SMS messages from a number in the list can be considered as a visual
+ * voicemail SMS. Otherwise, messages from any address will be considered.
+ */
+ public Builder setOriginatingNumbers(List<String> originatingNumbers) {
+ if (originatingNumbers == null) {
+ throw new IllegalArgumentException("Originating numbers cannot be null");
+ }
+ mOriginatingNumbers = originatingNumbers;
+ return this;
+ }
+
+ /**
+ * Sets the destination port for the visual voicemail SMS filter.
+ *
+ * @param destinationPort The destination port, or {@link #DESTINATION_PORT_ANY}, or {@link
+ * #DESTINATION_PORT_DATA_SMS}
+ */
+ public Builder setDestinationPort(int destinationPort) {
+ mDestinationPort = destinationPort;
+ return this;
+ }
+
+ }
+
+ /**
+ * The client prefix for the visual voicemail SMS filter. The client prefix will appear at the
+ * start of a visual voicemail SMS message, followed by a colon(:).
+ */
+ public final String clientPrefix;
+
+ /**
+ * The originating number whitelist for the visual voicemail SMS filter of a phone account. If
+ * the list is not null only the SMS messages from a number in the list can be considered as a
+ * visual voicemail SMS. Otherwise, messages from any address will be considered.
+ */
+ public final List<String> originatingNumbers;
+
+ /**
+ * The destination port for the visual voicemail SMS filter, or {@link #DESTINATION_PORT_ANY},
+ * or {@link #DESTINATION_PORT_DATA_SMS}
+ */
+ public final int destinationPort;
+
+ /**
+ * Use {@link Builder} to construct
+ */
+ private VisualVoicemailSmsFilterSettings(Builder builder) {
+ clientPrefix = builder.mClientPrefix;
+ originatingNumbers = builder.mOriginatingNumbers;
+ destinationPort = builder.mDestinationPort;
+ }
+
+ public static final Creator<VisualVoicemailSmsFilterSettings> CREATOR =
+ new Creator<VisualVoicemailSmsFilterSettings>() {
+ @Override
+ public VisualVoicemailSmsFilterSettings createFromParcel(Parcel in) {
+ Builder builder = new Builder();
+ builder.setClientPrefix(in.readString());
+ builder.setOriginatingNumbers(in.createStringArrayList());
+ builder.setDestinationPort(in.readInt());
+
+ return builder.build();
+ }
+
+ @Override
+ public VisualVoicemailSmsFilterSettings[] newArray(int size) {
+ return new VisualVoicemailSmsFilterSettings[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(clientPrefix);
+ dest.writeStringList(originatingNumbers);
+ dest.writeInt(destinationPort);
+ }
+
+}
diff --git a/telephony/java/com/android/ims/internal/IImsService.aidl b/telephony/java/com/android/ims/internal/IImsService.aidl
index a9614a6..406d22d 100644
--- a/telephony/java/com/android/ims/internal/IImsService.aidl
+++ b/telephony/java/com/android/ims/internal/IImsService.aidl
@@ -38,8 +38,19 @@
void close(int serviceId);
boolean isConnected(int serviceId, int serviceType, int callType);
boolean isOpened(int serviceId);
+
+ /**
+ * Replace existing registration listener
+ *
+ */
void setRegistrationListener(int serviceId, in IImsRegistrationListener listener);
+ /**
+ * Add new registration listener
+ */
+ void addRegistrationListener(int phoneId, int serviceClass,
+ in IImsRegistrationListener listener);
+
ImsCallProfile createCallProfile(int serviceId, int serviceType, int callType);
IImsCallSession createCallSession(int serviceId, in ImsCallProfile profile,
diff --git a/telephony/java/com/android/internal/telephony/DctConstants.java b/telephony/java/com/android/internal/telephony/DctConstants.java
index e4981ce..8166e00 100644
--- a/telephony/java/com/android/internal/telephony/DctConstants.java
+++ b/telephony/java/com/android/internal/telephony/DctConstants.java
@@ -104,6 +104,7 @@
public static final int CMD_CLEAR_PROVISIONING_SPINNER = BASE + 42;
public static final int EVENT_DEVICE_PROVISIONED_CHANGE = BASE + 43;
public static final int EVENT_REDIRECTION_DETECTED = BASE + 44;
+ public static final int EVENT_PCO_DATA_RECEIVED = BASE + 45;
/***** Constants *****/
diff --git a/telephony/java/com/android/internal/telephony/ISms.aidl b/telephony/java/com/android/internal/telephony/ISms.aidl
index 70a8653..6115656 100644
--- a/telephony/java/com/android/internal/telephony/ISms.aidl
+++ b/telephony/java/com/android/internal/telephony/ISms.aidl
@@ -184,7 +184,7 @@
*/
void sendTextForSubscriberWithSelfPermissions(in int subId, String callingPkg,
in String destAddr, in String scAddr, in String text, in PendingIntent sentIntent,
- in PendingIntent deliveryIntent);
+ in PendingIntent deliveryIntent, in boolean persistMessage);
/**
* Inject an SMS PDU into the android platform.
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index c61ed2a..f6aef08 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -118,17 +118,6 @@
int setDisplayName(String displayName, int subId);
/**
- * Set Sim Provisioning Status by subscription ID
- * @param simProvisionStatus with the subscription:
- * {@See SubscriptionManager#SIM_PROVISIONED}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_COLD}
- * {@See SubscriptionManager#SIM_UNPROVISIONED_OUT_OF_CREDIT}
- * @param subId the unique SubInfoRecord index in database
- * @return the number of records updated
- */
- int setSimProvisioningStatus(int simProvisioningStatus, int subId);
-
- /**
* Set display name by simInfo index with name source
* @param displayName the display name of SIM card
* @param subId the unique SubscriptionInfo index in database
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index bb8aaad5..2171c9e 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -20,6 +20,7 @@
import android.os.Bundle;
import android.os.ResultReceiver;
import android.net.Uri;
+import android.service.carrier.CarrierIdentifier;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.CellInfo;
@@ -28,8 +29,11 @@
import android.telephony.NeighboringCellInfo;
import android.telephony.RadioAccessFamily;
import android.telephony.ServiceState;
+import android.telephony.TelephonyHistogram;
+import android.telephony.VisualVoicemailSmsFilterSettings;
import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.OperatorInfo;
+
import java.util.List;
@@ -450,6 +454,20 @@
*/
int getVoiceMessageCountForSubscriber(int subId);
+ // Not oneway, caller needs to make sure the vaule is set before receiving a SMS
+ void enableVisualVoicemailSmsFilter(String callingPackage, int subId,
+ in VisualVoicemailSmsFilterSettings settings);
+
+ oneway void disableVisualVoicemailSmsFilter(String callingPackage, int subId);
+
+ // Get settings set by the calling package
+ VisualVoicemailSmsFilterSettings getVisualVoicemailSmsFilterSettings(String callingPackage,
+ int subId);
+
+ // Get settings set by the package, requires READ_PRIVILEGED_PHONE_STATE permission
+ VisualVoicemailSmsFilterSettings getSystemVisualVoicemailSmsFilterSettings(String packageName,
+ int subId);
+
/**
* Returns the network type for data transmission
* Legacy call, permission-free
@@ -1067,4 +1085,69 @@
* Returns a list of packages that have carrier privileges.
*/
List<String> getPackagesWithCarrierPrivileges();
+
+ /**
+ * Return the application ID for the app type.
+ *
+ * @param subId the subscription ID that this request applies to.
+ * @param appType the uicc app type,
+ * @return Application ID for specificied app type or null if no uicc or error.
+ */
+ String getAidForAppType(int subId, int appType);
+
+ /**
+ * Return the Electronic Serial Number.
+ *
+ * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
+ *
+ * @param subId the subscription ID that this request applies to.
+ * @return ESN or null if error.
+ * @hide
+ */
+ String getEsn(int subId);
+
+ /**
+ * Get snapshot of Telephony histograms
+ * @return List of Telephony histograms
+ * Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ */
+ List<TelephonyHistogram> getTelephonyHistograms();
+
+ /**
+ * Set the allowed carrier list for slotId
+ * Require system privileges. In the future we may add this to carrier APIs.
+ *
+ * @return The number of carriers set successfully. Should match length of
+ * carriers on success.
+ */
+ int setAllowedCarriers(int slotId, in List<CarrierIdentifier> carriers);
+
+ /**
+ * Get the allowed carrier list for slotId.
+ * Require system privileges. In the future we may add this to carrier APIs.
+ *
+ * @return List of {@link android.service.carrier.CarrierIdentifier}; empty list
+ * means all carriers are allowed.
+ */
+ List<CarrierIdentifier> getAllowedCarriers(int slotId);
+
+ /**
+ * Action set from carrier signalling broadcast receivers to enable/disable metered apns
+ * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
+ * @param subId the subscription ID that this action applies to.
+ * @param enabled control enable or disable metered apns.
+ * @hide
+ */
+ void carrierActionSetMeteredApnsEnabled(int subId, boolean visible);
+
+ /**
+ * Action set from carrier signalling broadcast receivers to enable/disable radio
+ * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
+ * @param subId the subscription ID that this action applies to.
+ * @param enabled control enable or disable radio.
+ * @hide
+ */
+ void carrierActionSetRadioEnabled(int subId, boolean enabled);
}
diff --git a/telephony/java/com/android/internal/telephony/IccCardConstants.java b/telephony/java/com/android/internal/telephony/IccCardConstants.java
index c1e2518..f3d9335 100644
--- a/telephony/java/com/android/internal/telephony/IccCardConstants.java
+++ b/telephony/java/com/android/internal/telephony/IccCardConstants.java
@@ -32,6 +32,8 @@
public static final String INTENT_VALUE_ICC_ABSENT = "ABSENT";
/* CARD_IO_ERROR means for three consecutive times there was SIM IO error */
static public final String INTENT_VALUE_ICC_CARD_IO_ERROR = "CARD_IO_ERROR";
+ /* CARD_RESTRICTED means card is present but not usable due to carrier restrictions */
+ static public final String INTENT_VALUE_ICC_CARD_RESTRICTED = "CARD_RESTRICTED";
/* LOCKED means ICC is locked by pin or by network */
public static final String INTENT_VALUE_ICC_LOCKED = "LOCKED";
//TODO: we can remove this state in the future if Bug 18489776 analysis
@@ -74,7 +76,8 @@
READY, /** ordinal(5) == {@See TelephonyManager#SIM_STATE_READY} */
NOT_READY, /** ordinal(6) == {@See TelephonyManager#SIM_STATE_NOT_READY} */
PERM_DISABLED, /** ordinal(7) == {@See TelephonyManager#SIM_STATE_PERM_DISABLED} */
- CARD_IO_ERROR; /** ordinal(8) == {@See TelephonyManager#SIM_STATE_CARD_IO_ERROR} */
+ CARD_IO_ERROR, /** ordinal(8) == {@See TelephonyManager#SIM_STATE_CARD_IO_ERROR} */
+ CARD_RESTRICTED;/** ordinal(9) == {@See TelephonyManager#SIM_STATE_CARD_RESTRICTED} */
public boolean isPinLocked() {
return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED));
@@ -83,7 +86,8 @@
public boolean iccCardExist() {
return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED)
|| (this == NETWORK_LOCKED) || (this == READY)
- || (this == PERM_DISABLED) || (this == CARD_IO_ERROR));
+ || (this == PERM_DISABLED) || (this == CARD_IO_ERROR)
+ || (this == CARD_RESTRICTED));
}
public static State intToState(int state) throws IllegalArgumentException {
@@ -97,6 +101,7 @@
case 6: return NOT_READY;
case 7: return PERM_DISABLED;
case 8: return CARD_IO_ERROR;
+ case 9: return CARD_RESTRICTED;
default:
throw new IllegalArgumentException();
}
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index cfc5305..a91e9be 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -409,6 +409,8 @@
int RIL_REQUEST_STOP_LCE = 133;
int RIL_REQUEST_PULL_LCEDATA = 134;
int RIL_REQUEST_GET_ACTIVITY_INFO = 135;
+ int RIL_REQUEST_SET_ALLOWED_CARRIERS = 136;
+ int RIL_REQUEST_GET_ALLOWED_CARRIERS = 137;
int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
@@ -459,4 +461,5 @@
int RIL_UNSOL_ON_SS = 1043;
int RIL_UNSOL_STK_CC_ALPHA_NOTIFY = 1044;
int RIL_UNSOL_LCEDATA_RECV = 1045;
+ int RIL_UNSOL_PCO_DATA = 1046;
}
diff --git a/telephony/java/com/android/internal/telephony/TelephonyIntents.java b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
index 46b0fbd..0168874 100644
--- a/telephony/java/com/android/internal/telephony/TelephonyIntents.java
+++ b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
@@ -402,34 +402,59 @@
/**
* <p>Broadcast Action: when data connections get redirected with validation failure.
* intended for sim/account status checks and only sent to the specified carrier app
- * feedback is via carrier/system APIs to report cold-sim, out-of-credit-sim, etc
* The intent will have the following extra values:</p>
* <ul>
- * <li>redirectUrl</li><dd>A string with the redirection url info.</dd>
- * <li>subId</li><dd>Sub Id which associated the data redirection.</dd>
+ * <li>apnType</li><dd>A string with the apn type.</dd>
+ * <li>redirectionUrl</li><dd>redirection url string</dd>
+ * <li>subId</dt><li>Sub Id which associated the data connection failure.</dd>
* </ul>
* <p class="note">This is a protected intent that can only be sent by the system.</p>
*/
- public static final String ACTION_DATA_CONNECTION_REDIRECTED =
- "android.intent.action.REDIRECTION_DETECTED";
+ public static final String ACTION_CARRIER_SIGNAL_REDIRECTED =
+ "android.intent.action.CARRIER_SIGNAL_REDIRECTED";
/**
* <p>Broadcast Action: when data connections setup fails.
* intended for sim/account status checks and only sent to the specified carrier app
- * feedback is via carrier/system APIs to report cold-sim, out-of-credit-sim, etc
* The intent will have the following extra values:</p>
* <ul>
* <li>apnType</li><dd>A string with the apn type.</dd>
* <li>errorCode</li><dd>A integer with dataFailCause.</dd>
- * <li>subId</dt><li>Sub Id which associated the data redirection.</dd>
+ * <li>subId</dt><li>Sub Id which associated the data connection failure.</dd>
* </ul>
* <p class="note">This is a protected intent that can only be sent by the system. </p>
*/
- public static final String ACTION_REQUEST_NETWORK_FAILED =
- "android.intent.action.REQUEST_NETWORK_FAILED";
+ public static final String ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED =
+ "android.intent.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED";
/**
- * Broadcast action to trigger CI OMA-DM Session.
+ * <p>Broadcast Action: when pco value is available.
+ * intended for sim/account status checks and only sent to the specified carrier app
+ * The intent will have the following extra values:</p>
+ * <ul>
+ * <li>apnType</li><dd>A string with the apn type.</dd>
+ * <li>apnProto</li><dd>A string with the protocol of the apn connection (IP,IPV6,
+ * IPV4V6)</dd>
+ * <li>pcoId</li><dd>An integer indicating the pco id for the data.</dd>
+ * <li>pcoValue</li><dd>A byte array of pco data read from modem.</dd>
+ * <li>subId</dt><li>Sub Id which associated the data connection.</dd>
+ * </ul>
+ * <p class="note">This is a protected intent that can only be sent by the system. </p>
*/
+ public static final String ACTION_CARRIER_SIGNAL_PCO_VALUE =
+ "android.intent.action.CARRIER_SIGNAL_PCO_VALUE";
+
+ // CARRIER_SIGNAL_ACTION extra keys
+ public static final String EXTRA_REDIRECTION_URL_KEY = "redirectionUrl";
+ public static final String EXTRA_ERROR_CODE_KEY = "errorCode";
+ public static final String EXTRA_APN_TYPE_KEY = "apnType";
+ public static final String EXTRA_APN_PROTO_KEY = "apnProto";
+ public static final String EXTRA_PCO_ID_KEY = "pcoId";
+ public static final String EXTRA_PCO_VALUE_KEY = "pcoValue";
+
+
+ /**
+ * Broadcast action to trigger CI OMA-DM Session.
+ */
public static final String ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE =
"com.android.omadm.service.CONFIGURATION_UPDATE";
}
diff --git a/tests/WallpaperTest/res/values/strings.xml b/tests/WallpaperTest/res/values/strings.xml
index fd21259..80a3d49 100644
--- a/tests/WallpaperTest/res/values/strings.xml
+++ b/tests/WallpaperTest/res/values/strings.xml
@@ -24,6 +24,9 @@
Test wallpaper for use with the wallpaper test app.
</string>
+ <string name="test_wallpaper_context_uri">https://www.google.com/maps/@37.8092876,-122.408986,1391m/data=!3m1!1e3</string>
+ <string name="test_wallpaper_context_description">Explore</string>
+
<string name="dimens">Dimens: </string>
<string name="width">Width: </string>
<string name="height">Height: </string>
diff --git a/tests/WallpaperTest/res/xml/test_wallpaper.xml b/tests/WallpaperTest/res/xml/test_wallpaper.xml
index 9f7d714..ba22478 100644
--- a/tests/WallpaperTest/res/xml/test_wallpaper.xml
+++ b/tests/WallpaperTest/res/xml/test_wallpaper.xml
@@ -23,4 +23,7 @@
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:author="@string/test_wallpaper_author"
android:description="@string/test_wallpaper_desc"
- android:thumbnail="@drawable/test_wallpaper_thumb" />
+ android:thumbnail="@drawable/test_wallpaper_thumb"
+ android:showMetadataInPreview="true"
+ android:contextUri="@string/test_wallpaper_context_uri"
+ android:contextDescription="@string/test_wallpaper_context_description"/>
diff --git a/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java b/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
index 95db6d1..ab36c22 100644
--- a/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
+++ b/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
@@ -144,6 +144,14 @@
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
+
+ // Simulate some slowness, so we can test the loading process in the live wallpaper
+ // picker.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
@Override
diff --git a/tests/utils/testutils/Android.mk b/tests/utils/testutils/Android.mk
new file mode 100644
index 0000000..d53167f
--- /dev/null
+++ b/tests/utils/testutils/Android.mk
@@ -0,0 +1,30 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := frameworks-base-testutils
+LOCAL_MODULE_TAG := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under,java)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ mockito-target
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java
new file mode 100644
index 0000000..746c77d
--- /dev/null
+++ b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java
@@ -0,0 +1,62 @@
+/*
+ * 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.app.test;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Utilities for creating Answers for mock objects
+ */
+public class MockAnswerUtil {
+
+ /**
+ * Answer that calls the method in the Answer called "answer" that matches the type signature of
+ * the method being answered. An error will be thrown at runtime if the signature does not match
+ * exactly.
+ */
+ public static class AnswerWithArguments implements Answer<Object> {
+ @Override
+ public final Object answer(InvocationOnMock invocation) throws Throwable {
+ Method method = invocation.getMethod();
+ try {
+ Method implementation = getClass().getMethod("answer", method.getParameterTypes());
+ if (!implementation.getReturnType().equals(method.getReturnType())) {
+ throw new RuntimeException("Found answer method does not have expected return "
+ + "type. Expected: " + method.getReturnType() + ", got "
+ + implementation.getReturnType());
+ }
+ Object[] args = invocation.getArguments();
+ try {
+ return implementation.invoke(this, args);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Error invoking answer method", e);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Could not find answer method with the expected args "
+ + Arrays.toString(method.getParameterTypes()), e);
+ }
+ }
+ }
+
+}
diff --git a/tests/utils/testutils/java/android/app/test/TestAlarmManager.java b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java
new file mode 100644
index 0000000..e90ea1e
--- /dev/null
+++ b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.test;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import android.app.AlarmManager;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Creates an AlarmManager whose alarm dispatch can be controlled
+ * Currently only supports alarm listeners
+ *
+ * Alarm listeners will be dispatched to the handler provided or will
+ * be dispatched immediately if they would have been sent to the main
+ * looper (handler was null).
+ */
+public class TestAlarmManager {
+ private final AlarmManager mAlarmManager;
+ private final List<PendingAlarm> mPendingAlarms;
+
+ public TestAlarmManager() throws Exception {
+ mPendingAlarms = new ArrayList<>();
+
+ mAlarmManager = mock(AlarmManager.class);
+ doAnswer(new SetListenerAnswer()).when(mAlarmManager).set(anyInt(), anyLong(), anyString(),
+ any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+ doAnswer(new SetListenerAnswer()).when(mAlarmManager).setExact(anyInt(), anyLong(),
+ anyString(), any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+ doAnswer(new CancelListenerAnswer())
+ .when(mAlarmManager).cancel(any(AlarmManager.OnAlarmListener.class));
+ }
+
+ public AlarmManager getAlarmManager() {
+ return mAlarmManager;
+ }
+
+ /**
+ * Dispatch a pending alarm with the given tag
+ * @return if any alarm was dispatched
+ */
+ public boolean dispatch(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ mPendingAlarms.remove(i);
+ alarm.dispatch();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return if an alarm with the given tag is pending
+ */
+ public boolean isPending(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return trigger time of an pending alarm with the given tag
+ * -1 if no pending alarm with the given tag
+ */
+ public long getTriggerTimeMillis(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ return alarm.getTriggerTimeMillis();
+ }
+ }
+ return -1;
+ }
+
+ private static class PendingAlarm {
+ private final int mType;
+ private final long mTriggerAtMillis;
+ private final String mTag;
+ private final Runnable mCallback;
+
+ public PendingAlarm(int type, long triggerAtMillis, String tag, Runnable callback) {
+ mType = type;
+ mTriggerAtMillis = triggerAtMillis;
+ mTag = tag;
+ mCallback = callback;
+ }
+
+ public void dispatch() {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ }
+
+ public Runnable getCallback() {
+ return mCallback;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ public long getTriggerTimeMillis() {
+ return mTriggerAtMillis;
+ }
+ }
+
+ private class SetListenerAnswer extends AnswerWithArguments {
+ public void answer(int type, long triggerAtMillis, String tag,
+ AlarmManager.OnAlarmListener listener, Handler handler) {
+ mPendingAlarms.add(new PendingAlarm(type, triggerAtMillis, tag,
+ new AlarmListenerRunnable(listener, handler)));
+ }
+ }
+
+ private class CancelListenerAnswer extends AnswerWithArguments {
+ public void answer(AlarmManager.OnAlarmListener listener) {
+ Iterator<PendingAlarm> alarmItr = mPendingAlarms.iterator();
+ while (alarmItr.hasNext()) {
+ PendingAlarm alarm = alarmItr.next();
+ if (alarm.getCallback() instanceof AlarmListenerRunnable) {
+ AlarmListenerRunnable alarmCallback =
+ (AlarmListenerRunnable) alarm.getCallback();
+ if (alarmCallback.getListener() == listener) {
+ alarmItr.remove();
+ }
+ }
+ }
+ }
+ }
+
+ private static class AlarmListenerRunnable implements Runnable {
+ private final AlarmManager.OnAlarmListener mListener;
+ private final Handler mHandler;
+ public AlarmListenerRunnable(AlarmManager.OnAlarmListener listener, Handler handler) {
+ mListener = listener;
+ mHandler = handler;
+ }
+
+ public AlarmManager.OnAlarmListener getListener() {
+ return mListener;
+ }
+
+ @Override
+ public void run() {
+ if (mHandler != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onAlarm();
+ }
+ });
+ } else { // normally gets dispatched in main looper
+ mListener.onAlarm();
+ }
+ }
+ }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
new file mode 100644
index 0000000..e8ceb4a
--- /dev/null
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.test;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Creates a looper whose message queue can be manipulated
+ * This allows testing code that uses a looper to dispatch messages in a deterministic manner
+ * Creating a TestLooper will also install it as the looper for the current thread
+ */
+public class TestLooper {
+ protected final Looper mLooper;
+
+ private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
+ private static final Field THREAD_LOCAL_LOOPER_FIELD;
+ private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
+ private static final Field MESSAGE_NEXT_FIELD;
+ private static final Field MESSAGE_WHEN_FIELD;
+ private static final Method MESSAGE_MARK_IN_USE_METHOD;
+ private static final String TAG = "TestLooper";
+
+ private AutoDispatchThread mAutoDispatchThread;
+
+ static {
+ try {
+ LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
+ LOOPER_CONSTRUCTOR.setAccessible(true);
+ THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
+ THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
+ MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+ MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+ MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+ MESSAGE_NEXT_FIELD.setAccessible(true);
+ MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+ MESSAGE_WHEN_FIELD.setAccessible(true);
+ MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+ MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+ } catch (NoSuchFieldException | NoSuchMethodException e) {
+ throw new RuntimeException("Failed to initialize TestLooper", e);
+ }
+ }
+
+
+ public TestLooper() {
+ try {
+ mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
+
+ ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
+ .get(null);
+ threadLocalLooper.set(mLooper);
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new RuntimeException("Reflection error constructing or accessing looper", e);
+ }
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ private Message getMessageLinkedList() {
+ try {
+ MessageQueue queue = mLooper.getQueue();
+ return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
+ e);
+ }
+ }
+
+ public void moveTimeForward(long milliSeconds) {
+ try {
+ Message msg = getMessageLinkedList();
+ while (msg != null) {
+ long updatedWhen = msg.getWhen() - milliSeconds;
+ if (updatedWhen < 0) {
+ updatedWhen = 0;
+ }
+ MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
+ msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
+ }
+ }
+
+ private Message messageQueueNext() {
+ try {
+ long now = SystemClock.uptimeMillis();
+
+ Message prevMsg = null;
+ Message msg = getMessageLinkedList();
+ if (msg != null && msg.getTarget() == null) {
+ // Stalled by a barrier. Find the next asynchronous message in
+ // the queue.
+ do {
+ prevMsg = msg;
+ msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+ } while (msg != null && !msg.isAsynchronous());
+ }
+ if (msg != null) {
+ if (now >= msg.getWhen()) {
+ // Got a message.
+ if (prevMsg != null) {
+ MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
+ } else {
+ MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
+ MESSAGE_NEXT_FIELD.get(msg));
+ }
+ MESSAGE_NEXT_FIELD.set(msg, null);
+ MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
+ return msg;
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Access failed in TestLooper", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return true if there are pending messages in the message queue
+ */
+ public synchronized boolean isIdle() {
+ Message messageList = getMessageLinkedList();
+
+ return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
+ }
+
+ /**
+ * @return the next message in the Looper's message queue or null if there is none
+ */
+ public synchronized Message nextMessage() {
+ if (isIdle()) {
+ return messageQueueNext();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Dispatch the next message in the queue
+ * Asserts that there is a message in the queue
+ */
+ public synchronized void dispatchNext() {
+ assertTrue(isIdle());
+ Message msg = messageQueueNext();
+ if (msg == null) {
+ return;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ /**
+ * Dispatch all messages currently in the queue
+ * Will not fail if there are no messages pending
+ * @return the number of messages dispatched
+ */
+ public synchronized int dispatchAll() {
+ int count = 0;
+ while (isIdle()) {
+ dispatchNext();
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Thread used to dispatch messages when the main thread is blocked waiting for a response.
+ */
+ private class AutoDispatchThread extends Thread {
+ private static final int MAX_LOOPS = 100;
+ private static final int LOOP_SLEEP_TIME_MS = 10;
+
+ private RuntimeException mAutoDispatchException = null;
+
+ /**
+ * Run method for the auto dispatch thread.
+ * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
+ * The thread continues looping and attempting to dispatch all messages until at
+ * least one message has been dispatched.
+ */
+ @Override
+ public void run() {
+ int dispatchCount = 0;
+ for (int i = 0; i < MAX_LOOPS; i++) {
+ try {
+ dispatchCount = dispatchAll();
+ } catch (RuntimeException e) {
+ mAutoDispatchException = e;
+ }
+ Log.d(TAG, "dispatched " + dispatchCount + " messages");
+ if (dispatchCount > 0) {
+ return;
+ }
+ try {
+ Thread.sleep(LOOP_SLEEP_TIME_MS);
+ } catch (InterruptedException e) {
+ mAutoDispatchException = new IllegalStateException(
+ "stopAutoDispatch called before any messages were dispatched.");
+ return;
+ }
+ }
+ Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
+ mAutoDispatchException = new IllegalStateException(
+ "TestLooper did not dispatch any messages before exiting.");
+ }
+
+ /**
+ * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
+ * to the main thread.
+ *
+ * @return RuntimeException Exception created by stopping without dispatching a message
+ */
+ public RuntimeException getException() {
+ return mAutoDispatchException;
+ }
+ }
+
+ /**
+ * Create and start a new AutoDispatchThread if one is not already running.
+ */
+ public void startAutoDispatch() {
+ if (mAutoDispatchThread != null) {
+ throw new IllegalStateException(
+ "startAutoDispatch called with the AutoDispatchThread already running.");
+ }
+ mAutoDispatchThread = new AutoDispatchThread();
+ mAutoDispatchThread.start();
+ }
+
+ /**
+ * If an AutoDispatchThread is currently running, stop and clean up.
+ */
+ public void stopAutoDispatch() {
+ if (mAutoDispatchThread != null) {
+ if (mAutoDispatchThread.isAlive()) {
+ mAutoDispatchThread.interrupt();
+ }
+ try {
+ mAutoDispatchThread.join();
+ } catch (InterruptedException e) {
+ // Catch exception from join.
+ }
+
+ RuntimeException e = mAutoDispatchThread.getException();
+ mAutoDispatchThread = null;
+ if (e != null) {
+ throw e;
+ }
+ } else {
+ // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
+ throw new IllegalStateException(
+ "stopAutoDispatch called without startAutoDispatch.");
+ }
+ }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooperTest.java b/tests/utils/testutils/java/android/os/test/TestLooperTest.java
new file mode 100644
index 0000000..40d83b5
--- /dev/null
+++ b/tests/utils/testutils/java/android/os/test/TestLooperTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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.os.test;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test TestLooperAbstractTime which provides control over "time". Note that
+ * real-time is being used as well. Therefore small time increments are NOT
+ * reliable. All tests are in "K" units (i.e. *1000).
+ */
+
+@SmallTest
+public class TestLooperTest {
+ private TestLooper mTestLooper;
+ private Handler mHandler;
+ private Handler mHandlerSpy;
+
+ @Rule
+ public ErrorCollector collector = new ErrorCollector();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTestLooper = new TestLooper();
+ mHandler = new Handler(mTestLooper.getLooper());
+ mHandlerSpy = spy(mHandler);
+ }
+
+ /**
+ * Basic test with no time stamps: dispatch 4 messages, check that all 4
+ * delivered (in correct order).
+ */
+ @Test
+ public void testNoTimeMovement() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageC));
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, A@10K. Don't move time.
+ * <p>
+ * Expected: only get A, B
+ */
+ @Test
+ public void testDelayedDispatchNoTimeMove() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, A@10K, Advance time by 5K.
+ * <p>
+ * Expected: only get A, B, C
+ */
+ @Test
+ public void testDelayedDispatchAdvanceTimeOnce() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+ mTestLooper.moveTimeForward(5000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, A@1K, B@2K Advance
+ * time by 1K.
+ * <p>
+ * Expected: get A, B, C, A
+ */
+ @Test
+ public void testDelayedDispatchAdvanceTimeTwice() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 1000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(1000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageA", messageA, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, A@5K, B@2K Advance
+ * time by 3K.
+ * <p>
+ * Expected: get A, B, C, B
+ */
+ @Test
+ public void testDelayedDispatchReverseOrder() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(3000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, dispatch all,
+ * A@5K, B@2K Advance time by 3K, dispatch all.
+ * <p>
+ * Expected: get A, B after first dispatch; then C, B after second dispatch
+ */
+ @Test
+ public void testDelayedDispatchAllMultipleTimes() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(3000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test AutoDispatch for a single message.
+ * This test would ideally use the Channel sendMessageSynchronously. At this time, the setup to
+ * get a working test channel is cumbersome. Until this is fixed, we substitute with a
+ * sendMessage followed by a blocking call. The main test thread blocks until the test handler
+ * receives the test message (messageA) and sets a boolean true. Once the boolean is true, the
+ * main thread will exit the busy wait loop, stop autoDispatch and check the assert.
+ *
+ * Enable AutoDispatch, add message, block on message being handled and stop AutoDispatch.
+ * <p>
+ * Expected: handleMessage is called for messageA and stopAutoDispatch is called.
+ */
+ @Test
+ public void testAutoDispatchWithSingleMessage() {
+ final int mLoopSleepTimeMs = 5;
+
+ final int messageA = 1;
+
+ TestLooper mockLooper = new TestLooper();
+ class TestHandler extends Handler {
+ public volatile boolean handledMessage = false;
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == messageA) {
+ handledMessage = true;
+ }
+ }
+ }
+
+ TestHandler testHandler = new TestHandler(mockLooper.getLooper());
+ mockLooper.startAutoDispatch();
+ testHandler.sendMessage(testHandler.obtainMessage(messageA));
+ while (!testHandler.handledMessage) {
+ // Block until message is handled
+ try {
+ Thread.sleep(mLoopSleepTimeMs);
+ } catch (InterruptedException e) {
+ // Interrupted while sleeping.
+ }
+ }
+ mockLooper.stopAutoDispatch();
+ assertTrue("TestHandler should have received messageA", testHandler.handledMessage);
+ }
+
+ /**
+ * Test starting AutoDispatch while already running throws IllegalStateException
+ * Enable AutoDispatch two times in a row.
+ * <p>
+ * Expected: catch IllegalStateException on second call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testRepeatedStartAutoDispatchThrowsException() {
+ mTestLooper.startAutoDispatch();
+ mTestLooper.startAutoDispatch();
+ }
+
+ /**
+ * Test stopping AutoDispatch without previously starting throws IllegalStateException
+ * Stop AutoDispatch
+ * <p>
+ * Expected: catch IllegalStateException on second call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testStopAutoDispatchWithoutStartThrowsException() {
+ mTestLooper.stopAutoDispatch();
+ }
+
+ /**
+ * Test AutoDispatch exits and does not dispatch a later message.
+ * Start and stop AutoDispatch then add a message.
+ * <p>
+ * Expected: After AutoDispatch is stopped, dispatchAll will return 1.
+ */
+ @Test
+ public void testAutoDispatchStopsCleanlyWithoutDispatchingAMessage() {
+ final int messageA = 1;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mTestLooper.startAutoDispatch();
+ try {
+ mTestLooper.stopAutoDispatch();
+ } catch (IllegalStateException e) {
+ // Stopping without a dispatch will throw an exception.
+ }
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ assertEquals("One message should be dispatched", 1, mTestLooper.dispatchAll());
+ }
+
+ /**
+ * Test AutoDispatch throws an exception when no messages are dispatched.
+ * Start and stop AutoDispatch
+ * <p>
+ * Expected: Exception is thrown with the stopAutoDispatch call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testAutoDispatchThrowsExceptionWhenNoMessagesDispatched() {
+ mTestLooper.startAutoDispatch();
+ mTestLooper.stopAutoDispatch();
+ }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java
new file mode 100644
index 0000000..25cd5b9
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java
@@ -0,0 +1,96 @@
+/*
+ * 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.internal.util.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+
+/**
+ * Provides an AsyncChannel interface that implements the connection initiating half of a
+ * bidirectional channel as described in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannel {
+ private static final String TAG = "BidirectionalAsyncChannel";
+
+ private AsyncChannel mChannel;
+ public enum ChannelState { DISCONNECTED, HALF_CONNECTED, CONNECTED, FAILURE };
+ private ChannelState mState = ChannelState.DISCONNECTED;
+
+ public void assertConnected() {
+ assertEquals("AsyncChannel was not fully connected", ChannelState.CONNECTED, mState);
+ }
+
+ public void connect(final Looper looper, final Messenger messenger,
+ final Handler incomingMessageHandler) {
+ assertEquals("AsyncChannel must be disconnected to connect",
+ ChannelState.DISCONNECTED, mState);
+ mChannel = new AsyncChannel();
+ Handler rawMessageHandler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+ if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+ Log.d(TAG, "Successfully half connected " + this);
+ mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+ mState = ChannelState.HALF_CONNECTED;
+ } else {
+ Log.d(TAG, "Failed to connect channel " + this);
+ mState = ChannelState.FAILURE;
+ mChannel = null;
+ }
+ break;
+ case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+ mState = ChannelState.CONNECTED;
+ Log.d(TAG, "Channel fully connected" + this);
+ break;
+ case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+ mState = ChannelState.DISCONNECTED;
+ mChannel = null;
+ Log.d(TAG, "Channel disconnected" + this);
+ break;
+ default:
+ incomingMessageHandler.handleMessage(msg);
+ break;
+ }
+ }
+ };
+ mChannel.connect(null, rawMessageHandler, messenger);
+ }
+
+ public void disconnect() {
+ assertEquals("AsyncChannel must be connected to disconnect",
+ ChannelState.CONNECTED, mState);
+ mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_DISCONNECT);
+ mState = ChannelState.DISCONNECTED;
+ mChannel = null;
+ }
+
+ public void sendMessage(Message msg) {
+ assertEquals("AsyncChannel must be connected to send messages",
+ ChannelState.CONNECTED, mState);
+ mChannel.sendMessage(msg);
+ }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java
new file mode 100644
index 0000000..49c8332
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java
@@ -0,0 +1,86 @@
+/*
+ * 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.internal.util.test;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an interface for the server side implementation of a bidirectional channel as described
+ * in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannelServer {
+
+ private static final String TAG = "BidirectionalAsyncChannelServer";
+
+ // Keeps track of incoming clients, which are identifiable by their messengers.
+ private final Map<Messenger, AsyncChannel> mClients = new HashMap<>();
+
+ private Messenger mMessenger;
+
+ public BidirectionalAsyncChannelServer(final Context context, final Looper looper,
+ final Handler messageHandler) {
+ Handler handler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncChannel channel = mClients.get(msg.replyTo);
+ switch (msg.what) {
+ case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
+ if (channel != null) {
+ Log.d(TAG, "duplicate client connection: " + msg.sendingUid);
+ channel.replyToMessage(msg,
+ AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+ AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
+ } else {
+ channel = new AsyncChannel();
+ mClients.put(msg.replyTo, channel);
+ channel.connected(context, this, msg.replyTo);
+ channel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+ AsyncChannel.STATUS_SUCCESSFUL);
+ }
+ break;
+ case AsyncChannel.CMD_CHANNEL_DISCONNECT:
+ channel.disconnect();
+ break;
+
+ case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+ mClients.remove(msg.replyTo);
+ break;
+
+ default:
+ messageHandler.handleMessage(msg);
+ break;
+ }
+ }
+ };
+ mMessenger = new Messenger(handler);
+ }
+
+ public Messenger getMessenger() {
+ return mMessenger;
+ }
+
+}
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 3a1e2bb..4b5ea65 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -115,6 +115,7 @@
toolSources := \
compile/Compile.cpp \
+ diff/Diff.cpp \
dump/Dump.cpp \
link/Link.cpp
diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp
index a2fadd9..00d8aae 100644
--- a/tools/aapt2/Main.cpp
+++ b/tools/aapt2/Main.cpp
@@ -24,6 +24,7 @@
extern int compile(const std::vector<StringPiece>& args);
extern int link(const std::vector<StringPiece>& args);
extern int dump(const std::vector<StringPiece>& args);
+extern int diff(const std::vector<StringPiece>& args);
} // namespace aapt
@@ -44,12 +45,14 @@
return aapt::link(args);
} else if (command == "dump" || command == "d") {
return aapt::dump(args);
+ } else if (command == "diff") {
+ return aapt::diff(args);
}
std::cerr << "unknown command '" << command << "'\n";
} else {
std::cerr << "no command specified\n";
}
- std::cerr << "\nusage: aapt2 [compile|link|dump] ..." << std::endl;
+ std::cerr << "\nusage: aapt2 [compile|link|dump|diff] ..." << std::endl;
return 1;
}
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 9704d970..a84c306 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -152,7 +152,7 @@
break;
}
- spanStack.back().lastChar = builder.str().size();
+ spanStack.back().lastChar = builder.str().size() - 1;
outStyleString->spans.push_back(spanStack.back());
spanStack.pop_back();
@@ -1058,6 +1058,16 @@
std::unique_ptr<Array> array = util::make_unique<Array>();
+ bool translateable = mOptions.translatable;
+ if (Maybe<StringPiece16> translateableAttr = xml::findAttribute(parser, u"translatable")) {
+ if (!ResourceUtils::tryParseBool(translateableAttr.value(), &translateable)) {
+ mDiag->error(DiagMessage(outResource->source)
+ << "invalid value for 'translatable'. Must be a boolean");
+ return false;
+ }
+ }
+ array->setTranslateable(translateable);
+
bool error = false;
const size_t depth = parser->getDepth();
while (xml::XmlPullParser::nextChildNode(parser, depth)) {
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 8d734f3..e700ed9 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -189,6 +189,17 @@
return results;
}
+std::vector<ResourceConfigValue*> ResourceEntry::findValuesIf(
+ const std::function<bool(ResourceConfigValue*)>& f) {
+ std::vector<ResourceConfigValue*> results;
+ for (auto& configValue : values) {
+ if (f(configValue.get())) {
+ results.push_back(configValue.get());
+ }
+ }
+ return results;
+}
+
/**
* The default handler for collisions. A return value of -1 means keep the
* existing value, 0 means fail, and +1 means take the incoming value.
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 7f5c2b8..5690ea6 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -26,6 +26,7 @@
#include "io/File.h"
#include <android-base/macros.h>
+#include <functional>
#include <map>
#include <memory>
#include <string>
@@ -109,6 +110,9 @@
ResourceConfigValue* findOrCreateValue(const ConfigDescription& config,
const StringPiece& product);
std::vector<ResourceConfigValue*> findAllValues(const ConfigDescription& config);
+ std::vector<ResourceConfigValue*> findValuesIf(
+ const std::function<bool(ResourceConfigValue*)>& f);
+
private:
DISALLOW_COPY_AND_ASSIGN(ResourceEntry);
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 74c48b0..a0a7efc 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -289,7 +289,7 @@
std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute* flagAttr,
const StringPiece16& str) {
android::Res_value flags = { };
- flags.dataType = android::Res_value::TYPE_INT_DEC;
+ flags.dataType = android::Res_value::TYPE_INT_HEX;
flags.data = 0u;
if (util::trimWhitespace(str).empty()) {
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index dd7ff01..c10b134 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -39,6 +39,14 @@
RawString::RawString(const StringPool::Ref& ref) : value(ref) {
}
+bool RawString::equals(const Value* value) const {
+ const RawString* other = valueCast<RawString>(value);
+ if (!other) {
+ return false;
+ }
+ return *this->value == *other->value;
+}
+
RawString* RawString::clone(StringPool* newPool) const {
RawString* rs = new RawString(newPool->makeRef(*value));
rs->mComment = mComment;
@@ -66,6 +74,15 @@
Reference::Reference(const ResourceId& i, Type type) : id(i), referenceType(type) {
}
+bool Reference::equals(const Value* value) const {
+ const Reference* other = valueCast<Reference>(value);
+ if (!other) {
+ return false;
+ }
+ return referenceType == other->referenceType && privateReference == other->privateReference &&
+ id == other->id && name == other->name;
+}
+
bool Reference::flatten(android::Res_value* outValue) const {
outValue->dataType = (referenceType == Reference::Type::kResource) ?
android::Res_value::TYPE_REFERENCE : android::Res_value::TYPE_ATTRIBUTE;
@@ -97,6 +114,10 @@
}
}
+bool Id::equals(const Value* value) const {
+ return valueCast<Id>(value) != nullptr;
+}
+
bool Id::flatten(android::Res_value* out) const {
out->dataType = android::Res_value::TYPE_INT_BOOLEAN;
out->data = util::hostToDevice32(0);
@@ -111,15 +132,15 @@
*out << "(id)";
}
-String::String(const StringPool::Ref& ref) : value(ref), mTranslateable(true) {
+String::String(const StringPool::Ref& ref) : value(ref) {
}
-void String::setTranslateable(bool val) {
- mTranslateable = val;
-}
-
-bool String::isTranslateable() const {
- return mTranslateable;
+bool String::equals(const Value* value) const {
+ const String* other = valueCast<String>(value);
+ if (!other) {
+ return false;
+ }
+ return *this->value == *other->value;
}
bool String::flatten(android::Res_value* outValue) const {
@@ -144,15 +165,24 @@
*out << "(string) \"" << *value << "\"";
}
-StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref), mTranslateable(true) {
+StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) {
}
-void StyledString::setTranslateable(bool val) {
- mTranslateable = val;
-}
+bool StyledString::equals(const Value* value) const {
+ const StyledString* other = valueCast<StyledString>(value);
+ if (!other) {
+ return false;
+ }
-bool StyledString::isTranslateable() const {
- return mTranslateable;
+ if (*this->value->str == *other->value->str) {
+ const std::vector<StringPool::Span>& spansA = this->value->spans;
+ const std::vector<StringPool::Span>& spansB = other->value->spans;
+ return std::equal(spansA.begin(), spansA.end(), spansB.begin(),
+ [](const StringPool::Span& a, const StringPool::Span& b) -> bool {
+ return *a.name == *b.name && a.firstChar == b.firstChar && a.lastChar == b.lastChar;
+ });
+ }
+ return false;
}
bool StyledString::flatten(android::Res_value* outValue) const {
@@ -174,11 +204,22 @@
void StyledString::print(std::ostream* out) const {
*out << "(styled string) \"" << *value->str << "\"";
+ for (const StringPool::Span& span : value->spans) {
+ *out << " "<< *span.name << ":" << span.firstChar << "," << span.lastChar;
+ }
}
FileReference::FileReference(const StringPool::Ref& _path) : path(_path) {
}
+bool FileReference::equals(const Value* value) const {
+ const FileReference* other = valueCast<FileReference>(value);
+ if (!other) {
+ return false;
+ }
+ return *path == *other->path;
+}
+
bool FileReference::flatten(android::Res_value* outValue) const {
if (path.getIndex() > std::numeric_limits<uint32_t>::max()) {
return false;
@@ -209,6 +250,14 @@
value.data = data;
}
+bool BinaryPrimitive::equals(const Value* value) const {
+ const BinaryPrimitive* other = valueCast<BinaryPrimitive>(value);
+ if (!other) {
+ return false;
+ }
+ return this->value.dataType == other->value.dataType && this->value.data == other->value.data;
+}
+
bool BinaryPrimitive::flatten(android::Res_value* outValue) const {
outValue->dataType = value.dataType;
outValue->data = util::hostToDevice32(value.data);
@@ -228,7 +277,7 @@
*out << "(integer) " << static_cast<int32_t>(value.data);
break;
case android::Res_value::TYPE_INT_HEX:
- *out << "(integer) " << std::hex << value.data << std::dec;
+ *out << "(integer) 0x" << std::hex << value.data << std::dec;
break;
case android::Res_value::TYPE_INT_BOOLEAN:
*out << "(boolean) " << (value.data != 0 ? "true" : "false");
@@ -253,6 +302,21 @@
mWeak = w;
}
+bool Attribute::equals(const Value* value) const {
+ const Attribute* other = valueCast<Attribute>(value);
+ if (!other) {
+ return false;
+ }
+
+ return this->typeMask == other->typeMask && this->minInt == other->minInt &&
+ this->maxInt == other->maxInt &&
+ std::equal(this->symbols.begin(), this->symbols.end(),
+ other->symbols.begin(),
+ [](const Symbol& a, const Symbol& b) -> bool {
+ return a.symbol.equals(&b.symbol) && a.value == b.value;
+ });
+}
+
Attribute* Attribute::clone(StringPool* /*newPool*/) const {
return new Attribute(*this);
}
@@ -365,6 +429,14 @@
<< "]";
}
+ if (minInt != std::numeric_limits<int32_t>::min()) {
+ *out << " min=" << minInt;
+ }
+
+ if (maxInt != std::numeric_limits<int32_t>::max()) {
+ *out << " max=" << maxInt;
+ }
+
if (isWeak()) {
*out << " [weak]";
}
@@ -445,6 +517,21 @@
return true;
}
+bool Style::equals(const Value* value) const {
+ const Style* other = valueCast<Style>(value);
+ if (!other) {
+ return false;
+ }
+ if (bool(parent) != bool(other->parent) ||
+ (parent && other->parent && !parent.value().equals(&other->parent.value()))) {
+ return false;
+ }
+ return std::equal(entries.begin(), entries.end(), other->entries.begin(),
+ [](const Entry& a, const Entry& b) -> bool {
+ return a.key.equals(&b.key) && a.value->equals(b.value.get());
+ });
+}
+
Style* Style::clone(StringPool* newPool) const {
Style* style = new Style();
style->parent = parent;
@@ -484,6 +571,18 @@
return out;
}
+bool Array::equals(const Value* value) const {
+ const Array* other = valueCast<Array>(value);
+ if (!other) {
+ return false;
+ }
+
+ return std::equal(items.begin(), items.end(), other->items.begin(),
+ [](const std::unique_ptr<Item>& a, const std::unique_ptr<Item>& b) -> bool {
+ return a->equals(b.get());
+ });
+}
+
Array* Array::clone(StringPool* newPool) const {
Array* array = new Array();
array->mComment = mComment;
@@ -500,6 +599,21 @@
<< "]";
}
+bool Plural::equals(const Value* value) const {
+ const Plural* other = valueCast<Plural>(value);
+ if (!other) {
+ return false;
+ }
+
+ return std::equal(values.begin(), values.end(), other->values.begin(),
+ [](const std::unique_ptr<Item>& a, const std::unique_ptr<Item>& b) -> bool {
+ if (bool(a) != bool(b)) {
+ return false;
+ }
+ return bool(a) == bool(b) || a->equals(b.get());
+ });
+}
+
Plural* Plural::clone(StringPool* newPool) const {
Plural* p = new Plural();
p->mComment = mComment;
@@ -515,12 +629,42 @@
void Plural::print(std::ostream* out) const {
*out << "(plural)";
+ if (values[Zero]) {
+ *out << " zero=" << *values[Zero];
+ }
+
+ if (values[One]) {
+ *out << " one=" << *values[One];
+ }
+
+ if (values[Two]) {
+ *out << " two=" << *values[Two];
+ }
+
+ if (values[Few]) {
+ *out << " few=" << *values[Few];
+ }
+
+ if (values[Many]) {
+ *out << " many=" << *values[Many];
+ }
}
static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Item>& item) {
return out << *item;
}
+bool Styleable::equals(const Value* value) const {
+ const Styleable* other = valueCast<Styleable>(value);
+ if (!other) {
+ return false;
+ }
+ return std::equal(entries.begin(), entries.end(), other->entries.begin(),
+ [](const Reference& a, const Reference& b) -> bool {
+ return a.equals(&b);
+ });
+}
+
Styleable* Styleable::clone(StringPool* /*newPool*/) const {
return new Styleable(*this);
}
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index 43354ac..aa1b550 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -54,6 +54,18 @@
mWeak = val;
}
+ // Whether the value is marked as translateable.
+ // This does not persist when flattened.
+ // It is only used during compilation phase.
+ void setTranslateable(bool val) {
+ mTranslateable = val;
+ }
+
+ // Default true.
+ bool isTranslateable() const {
+ return mTranslateable;
+ }
+
/**
* Returns the source where this value was defined.
*/
@@ -84,6 +96,8 @@
mComment = std::move(str);
}
+ virtual bool equals(const Value* value) const = 0;
+
/**
* Calls the appropriate overload of ValueVisitor.
*/
@@ -103,6 +117,7 @@
Source mSource;
std::u16string mComment;
bool mWeak = false;
+ bool mTranslateable = true;
};
/**
@@ -158,6 +173,7 @@
explicit Reference(const ResourceNameRef& n, Type type = Type::kResource);
explicit Reference(const ResourceId& i, Type type = Type::kResource);
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
Reference* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
@@ -168,6 +184,7 @@
*/
struct Id : public BaseItem<Id> {
Id() { mWeak = true; }
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* out) const override;
Id* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
@@ -183,6 +200,7 @@
RawString(const StringPool::Ref& ref);
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
RawString* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
@@ -193,17 +211,10 @@
String(const StringPool::Ref& ref);
- // Whether the string is marked as translateable. This does not persist when flattened.
- // It is only used during compilation phase.
- void setTranslateable(bool val);
- bool isTranslateable() const;
-
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
String* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
-
-private:
- bool mTranslateable;
};
struct StyledString : public BaseItem<StyledString> {
@@ -211,17 +222,10 @@
StyledString(const StringPool::StyleRef& ref);
- // Whether the string is marked as translateable. This does not persist when flattened.
- // It is only used during compilation phase.
- void setTranslateable(bool val);
- bool isTranslateable() const;
-
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
StyledString* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
-
-private:
- bool mTranslateable;
};
struct FileReference : public BaseItem<FileReference> {
@@ -235,6 +239,7 @@
FileReference() = default;
FileReference(const StringPool::Ref& path);
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
FileReference* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
@@ -250,6 +255,7 @@
BinaryPrimitive(const android::Res_value& val);
BinaryPrimitive(uint8_t dataType, uint32_t data);
+ bool equals(const Value* value) const override;
bool flatten(android::Res_value* outValue) const override;
BinaryPrimitive* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
@@ -268,6 +274,7 @@
Attribute(bool w, uint32_t t = 0u);
+ bool equals(const Value* value) const override;
Attribute* clone(StringPool* newPool) const override;
void printMask(std::ostream* out) const;
void print(std::ostream* out) const override;
@@ -290,6 +297,7 @@
std::vector<Entry> entries;
+ bool equals(const Value* value) const override;
Style* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
};
@@ -297,6 +305,7 @@
struct Array : public BaseValue<Array> {
std::vector<std::unique_ptr<Item>> items;
+ bool equals(const Value* value) const override;
Array* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
};
@@ -314,6 +323,7 @@
std::array<std::unique_ptr<Item>, Count> values;
+ bool equals(const Value* value) const override;
Plural* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
};
@@ -321,6 +331,7 @@
struct Styleable : public BaseValue<Styleable> {
std::vector<Reference> entries;
+ bool equals(const Value* value) const override;
Styleable* clone(StringPool* newPool) const override;
void print(std::ostream* out) const override;
};
diff --git a/tools/aapt2/ValueVisitor.h b/tools/aapt2/ValueVisitor.h
index ea2aa55..b8bc5db 100644
--- a/tools/aapt2/ValueVisitor.h
+++ b/tools/aapt2/ValueVisitor.h
@@ -127,6 +127,11 @@
}
};
+template <typename T>
+const T* valueCast(const Value* value) {
+ return valueCast<T>(const_cast<Value*>(value));
+}
+
/**
* Returns a valid pointer to T if the Value is of subtype T.
* Otherwise, returns nullptr.
@@ -141,7 +146,6 @@
return visitor.value;
}
-
inline void visitAllValuesInPackage(ResourceTablePackage* pkg, RawValueVisitor* visitor) {
for (auto& type : pkg->types) {
for (auto& entry : type->entries) {
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp
index 99c2077..d080e16 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp
@@ -128,23 +128,6 @@
mPool(pool), mMethod(method), mLocalizer(method) {
}
- void visit(Array* array) override {
- std::unique_ptr<Array> localized = util::make_unique<Array>();
- localized->items.resize(array->items.size());
- for (size_t i = 0; i < array->items.size(); i++) {
- Visitor subVisitor(mPool, mMethod);
- array->items[i]->accept(&subVisitor);
- if (subVisitor.mItem) {
- localized->items[i] = std::move(subVisitor.mItem);
- } else {
- localized->items[i] = std::unique_ptr<Item>(array->items[i]->clone(mPool));
- }
- }
- localized->setSource(array->getSource());
- localized->setWeak(true);
- mValue = std::move(localized);
- }
-
void visit(Plural* plural) override {
std::unique_ptr<Plural> localized = util::make_unique<Plural>();
for (size_t i = 0; i < plural->values.size(); i++) {
@@ -164,10 +147,6 @@
}
void visit(String* string) override {
- if (!string->isTranslateable()) {
- return;
- }
-
std::u16string result = mLocalizer.start() + mLocalizer.text(*string->value) +
mLocalizer.end();
std::unique_ptr<String> localized = util::make_unique<String>(mPool->makeRef(result));
@@ -177,10 +156,6 @@
}
void visit(StyledString* string) override {
- if (!string->isTranslateable()) {
- return;
- }
-
mItem = pseudolocalizeStyledString(string, mMethod, mPool);
mItem->setWeak(true);
}
@@ -238,14 +213,26 @@
}
}
+/**
+ * A value is pseudolocalizable if it does not define a locale (or is the default locale)
+ * and is translateable.
+ */
+static bool isPseudolocalizable(ResourceConfigValue* configValue) {
+ const int diff = configValue->config.diff(ConfigDescription::defaultConfig());
+ if (diff & ConfigDescription::CONFIG_LOCALE) {
+ return false;
+ }
+ return configValue->value->isTranslateable();
+}
+
} // namespace
bool PseudolocaleGenerator::consume(IAaptContext* context, ResourceTable* table) {
for (auto& package : table->packages) {
for (auto& type : package->types) {
for (auto& entry : type->entries) {
- std::vector<ResourceConfigValue*> values = entry->findAllValues(
- ConfigDescription::defaultConfig());
+ std::vector<ResourceConfigValue*> values = entry->findValuesIf(isPseudolocalizable);
+
for (ResourceConfigValue* value : values) {
pseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value,
&table->stringPool, entry.get());
diff --git a/tools/aapt2/diff/Diff.cpp b/tools/aapt2/diff/Diff.cpp
new file mode 100644
index 0000000..20b7b59
--- /dev/null
+++ b/tools/aapt2/diff/Diff.cpp
@@ -0,0 +1,411 @@
+/*
+ * 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 "Flags.h"
+#include "ResourceTable.h"
+#include "io/ZipArchive.h"
+#include "process/IResourceTableConsumer.h"
+#include "process/SymbolTable.h"
+#include "unflatten/BinaryResourceParser.h"
+
+#include <android-base/macros.h>
+
+namespace aapt {
+
+class DiffContext : public IAaptContext {
+public:
+ const std::u16string& getCompilationPackage() override {
+ return mEmpty;
+ }
+
+ uint8_t getPackageId() override {
+ return 0x0;
+ }
+
+ IDiagnostics* getDiagnostics() override {
+ return &mDiagnostics;
+ }
+
+ NameMangler* getNameMangler() override {
+ return &mNameMangler;
+ }
+
+ SymbolTable* getExternalSymbols() override {
+ return &mSymbolTable;
+ }
+
+ bool verbose() override {
+ return false;
+ }
+
+private:
+ std::u16string mEmpty;
+ StdErrDiagnostics mDiagnostics;
+ NameMangler mNameMangler = NameMangler(NameManglerPolicy{});
+ SymbolTable mSymbolTable;
+};
+
+class LoadedApk {
+public:
+ LoadedApk(const Source& source, std::unique_ptr<io::IFileCollection> apk,
+ std::unique_ptr<ResourceTable> table) :
+ mSource(source), mApk(std::move(apk)), mTable(std::move(table)) {
+ }
+
+ io::IFileCollection* getFileCollection() {
+ return mApk.get();
+ }
+
+ ResourceTable* getResourceTable() {
+ return mTable.get();
+ }
+
+ const Source& getSource() {
+ return mSource;
+ }
+
+private:
+ Source mSource;
+ std::unique_ptr<io::IFileCollection> mApk;
+ std::unique_ptr<ResourceTable> mTable;
+
+ DISALLOW_COPY_AND_ASSIGN(LoadedApk);
+};
+
+static std::unique_ptr<LoadedApk> loadApkFromPath(IAaptContext* context, const StringPiece& path) {
+ Source source(path);
+ std::string error;
+ std::unique_ptr<io::ZipFileCollection> apk = io::ZipFileCollection::create(path, &error);
+ if (!apk) {
+ context->getDiagnostics()->error(DiagMessage(source) << error);
+ return {};
+ }
+
+ io::IFile* file = apk->findFile("resources.arsc");
+ if (!file) {
+ context->getDiagnostics()->error(DiagMessage(source) << "no resources.arsc found");
+ return {};
+ }
+
+ std::unique_ptr<io::IData> data = file->openAsData();
+ if (!data) {
+ context->getDiagnostics()->error(DiagMessage(source) << "could not open resources.arsc");
+ return {};
+ }
+
+ std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
+ BinaryResourceParser parser(context, table.get(), source, data->data(), data->size());
+ if (!parser.parse()) {
+ return {};
+ }
+
+ return util::make_unique<LoadedApk>(source, std::move(apk), std::move(table));
+}
+
+static void emitDiffLine(const Source& source, const StringPiece& message) {
+ std::cerr << source << ": " << message << "\n";
+}
+
+static bool isSymbolVisibilityDifferent(const Symbol& symbolA, const Symbol& symbolB) {
+ return symbolA.state != symbolB.state;
+}
+
+template <typename Id>
+static bool isIdDiff(const Symbol& symbolA, const Maybe<Id>& idA,
+ const Symbol& symbolB, const Maybe<Id>& idB) {
+ if (symbolA.state == SymbolState::kPublic || symbolB.state == SymbolState::kPublic) {
+ return idA != idB;
+ }
+ return false;
+}
+
+static bool emitResourceConfigValueDiff(IAaptContext* context,
+ LoadedApk* apkA,
+ ResourceTablePackage* pkgA,
+ ResourceTableType* typeA,
+ ResourceEntry* entryA,
+ ResourceConfigValue* configValueA,
+ LoadedApk* apkB,
+ ResourceTablePackage* pkgB,
+ ResourceTableType* typeB,
+ ResourceEntry* entryB,
+ ResourceConfigValue* configValueB) {
+ Value* valueA = configValueA->value.get();
+ Value* valueB = configValueB->value.get();
+ if (!valueA->equals(valueB)) {
+ std::stringstream strStream;
+ strStream << "value " << pkgA->name << ":" << typeA->type << "/" << entryA->name
+ << " config=" << configValueA->config << " does not match:\n";
+ valueA->print(&strStream);
+ strStream << "\n vs \n";
+ valueB->print(&strStream);
+ emitDiffLine(apkB->getSource(), strStream.str());
+ return true;
+ }
+ return false;
+}
+
+static bool emitResourceEntryDiff(IAaptContext* context,
+ LoadedApk* apkA,
+ ResourceTablePackage* pkgA,
+ ResourceTableType* typeA,
+ ResourceEntry* entryA,
+ LoadedApk* apkB,
+ ResourceTablePackage* pkgB,
+ ResourceTableType* typeB,
+ ResourceEntry* entryB) {
+ bool diff = false;
+ for (std::unique_ptr<ResourceConfigValue>& configValueA : entryA->values) {
+ ResourceConfigValue* configValueB = entryB->findValue(configValueA->config);
+ if (!configValueB) {
+ std::stringstream strStream;
+ strStream << "missing " << pkgA->name << ":" << typeA->type << "/" << entryA->name
+ << " config=" << configValueA->config;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ } else {
+ diff |= emitResourceConfigValueDiff(context, apkA, pkgA, typeA, entryA,
+ configValueA.get(), apkB, pkgB, typeB, entryB,
+ configValueB);
+ }
+ }
+
+ // Check for any newly added config values.
+ for (std::unique_ptr<ResourceConfigValue>& configValueB : entryB->values) {
+ ResourceConfigValue* configValueA = entryA->findValue(configValueB->config);
+ if (!configValueA) {
+ std::stringstream strStream;
+ strStream << "new config " << pkgB->name << ":" << typeB->type << "/" << entryB->name
+ << " config=" << configValueB->config;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ }
+ return false;
+}
+
+static bool emitResourceTypeDiff(IAaptContext* context,
+ LoadedApk* apkA,
+ ResourceTablePackage* pkgA,
+ ResourceTableType* typeA,
+ LoadedApk* apkB,
+ ResourceTablePackage* pkgB,
+ ResourceTableType* typeB) {
+ bool diff = false;
+ for (std::unique_ptr<ResourceEntry>& entryA : typeA->entries) {
+ ResourceEntry* entryB = typeB->findEntry(entryA->name);
+ if (!entryB) {
+ std::stringstream strStream;
+ strStream << "missing " << pkgA->name << ":" << typeA->type << "/" << entryA->name;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ } else {
+ if (isSymbolVisibilityDifferent(entryA->symbolStatus, entryB->symbolStatus)) {
+ std::stringstream strStream;
+ strStream << pkgA->name << ":" << typeA->type << "/" << entryA->name
+ << " has different visibility (";
+ if (entryB->symbolStatus.state == SymbolState::kPublic) {
+ strStream << "PUBLIC";
+ } else {
+ strStream << "PRIVATE";
+ }
+ strStream << " vs ";
+ if (entryA->symbolStatus.state == SymbolState::kPublic) {
+ strStream << "PUBLIC";
+ } else {
+ strStream << "PRIVATE";
+ }
+ strStream << ")";
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ } else if (isIdDiff(entryA->symbolStatus, entryA->id,
+ entryB->symbolStatus, entryB->id)) {
+ std::stringstream strStream;
+ strStream << pkgA->name << ":" << typeA->type << "/" << entryA->name
+ << " has different public ID (";
+ if (entryB->id) {
+ strStream << "0x" << std::hex << entryB->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << " vs ";
+ if (entryA->id) {
+ strStream << "0x " << std::hex << entryA->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << ")";
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ diff |= emitResourceEntryDiff(context, apkA, pkgA, typeA, entryA.get(),
+ apkB, pkgB, typeB, entryB);
+ }
+ }
+
+ // Check for any newly added entries.
+ for (std::unique_ptr<ResourceEntry>& entryB : typeB->entries) {
+ ResourceEntry* entryA = typeA->findEntry(entryB->name);
+ if (!entryA) {
+ std::stringstream strStream;
+ strStream << "new entry " << pkgB->name << ":" << typeB->type << "/" << entryB->name;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ }
+ return diff;
+}
+
+static bool emitResourcePackageDiff(IAaptContext* context, LoadedApk* apkA,
+ ResourceTablePackage* pkgA,
+ LoadedApk* apkB, ResourceTablePackage* pkgB) {
+ bool diff = false;
+ for (std::unique_ptr<ResourceTableType>& typeA : pkgA->types) {
+ ResourceTableType* typeB = pkgB->findType(typeA->type);
+ if (!typeB) {
+ std::stringstream strStream;
+ strStream << "missing " << pkgA->name << ":" << typeA->type;
+ emitDiffLine(apkA->getSource(), strStream.str());
+ diff = true;
+ } else {
+ if (isSymbolVisibilityDifferent(typeA->symbolStatus, typeB->symbolStatus)) {
+ std::stringstream strStream;
+ strStream << pkgA->name << ":" << typeA->type << " has different visibility (";
+ if (typeB->symbolStatus.state == SymbolState::kPublic) {
+ strStream << "PUBLIC";
+ } else {
+ strStream << "PRIVATE";
+ }
+ strStream << " vs ";
+ if (typeA->symbolStatus.state == SymbolState::kPublic) {
+ strStream << "PUBLIC";
+ } else {
+ strStream << "PRIVATE";
+ }
+ strStream << ")";
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ } else if (isIdDiff(typeA->symbolStatus, typeA->id, typeB->symbolStatus, typeB->id)) {
+ std::stringstream strStream;
+ strStream << pkgA->name << ":" << typeA->type << " has different public ID (";
+ if (typeB->id) {
+ strStream << "0x" << std::hex << typeB->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << " vs ";
+ if (typeA->id) {
+ strStream << "0x " << std::hex << typeA->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << ")";
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ diff |= emitResourceTypeDiff(context, apkA, pkgA, typeA.get(), apkB, pkgB, typeB);
+ }
+ }
+
+ // Check for any newly added types.
+ for (std::unique_ptr<ResourceTableType>& typeB : pkgB->types) {
+ ResourceTableType* typeA = pkgA->findType(typeB->type);
+ if (!typeA) {
+ std::stringstream strStream;
+ strStream << "new type " << pkgB->name << ":" << typeB->type;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ }
+ return diff;
+}
+
+static bool emitResourceTableDiff(IAaptContext* context, LoadedApk* apkA, LoadedApk* apkB) {
+ ResourceTable* tableA = apkA->getResourceTable();
+ ResourceTable* tableB = apkB->getResourceTable();
+
+ bool diff = false;
+ for (std::unique_ptr<ResourceTablePackage>& pkgA : tableA->packages) {
+ ResourceTablePackage* pkgB = tableB->findPackage(pkgA->name);
+ if (!pkgB) {
+ std::stringstream strStream;
+ strStream << "missing package " << pkgA->name;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ } else {
+ if (pkgA->id != pkgB->id) {
+ std::stringstream strStream;
+ strStream << "package '" << pkgA->name << "' has different id (";
+ if (pkgB->id) {
+ strStream << "0x" << std::hex << pkgB->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << " vs ";
+ if (pkgA->id) {
+ strStream << "0x" << std::hex << pkgA->id.value();
+ } else {
+ strStream << "none";
+ }
+ strStream << ")";
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ diff |= emitResourcePackageDiff(context, apkA, pkgA.get(), apkB, pkgB);
+ }
+ }
+
+ // Check for any newly added packages.
+ for (std::unique_ptr<ResourceTablePackage>& pkgB : tableB->packages) {
+ ResourceTablePackage* pkgA = tableA->findPackage(pkgB->name);
+ if (!pkgA) {
+ std::stringstream strStream;
+ strStream << "new package " << pkgB->name;
+ emitDiffLine(apkB->getSource(), strStream.str());
+ diff = true;
+ }
+ }
+ return diff;
+}
+
+int diff(const std::vector<StringPiece>& args) {
+ DiffContext context;
+
+ Flags flags;
+ if (!flags.parse("aapt2 diff", args, &std::cerr)) {
+ return 1;
+ }
+
+ if (flags.getArgs().size() != 2u) {
+ std::cerr << "must have two apks as arguments.\n\n";
+ flags.usage("aapt2 diff", &std::cerr);
+ return 1;
+ }
+
+ std::unique_ptr<LoadedApk> apkA = loadApkFromPath(&context, flags.getArgs()[0]);
+ std::unique_ptr<LoadedApk> apkB = loadApkFromPath(&context, flags.getArgs()[1]);
+ if (!apkA || !apkB) {
+ return 1;
+ }
+
+ if (emitResourceTableDiff(&context, apkA.get(), apkB.get())) {
+ // We emitted a diff, so return 1 (failure).
+ return 1;
+ }
+ return 0;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 953e87e..77a949f 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -180,7 +180,6 @@
manifestAction[u"uses-configuration"];
manifestAction[u"uses-feature"];
- manifestAction[u"uses-library"];
manifestAction[u"supports-screens"];
manifestAction[u"compatible-screens"];
manifestAction[u"supports-gl-texture"];
@@ -189,6 +188,9 @@
xml::XmlNodeAction& applicationAction = (*executor)[u"manifest"][u"application"];
applicationAction.action(optionalNameIsJavaClassName);
+ // Uses library actions.
+ applicationAction[u"uses-library"];
+
// Activity actions.
applicationAction[u"activity"].action(requiredNameIsJavaClassName);
applicationAction[u"activity"][u"intent-filter"] = intentFilterAction;
diff --git a/tools/aapt2/proto/TableProtoDeserializer.cpp b/tools/aapt2/proto/TableProtoDeserializer.cpp
index 82e4fb0..1ec48f0 100644
--- a/tools/aapt2/proto/TableProtoDeserializer.cpp
+++ b/tools/aapt2/proto/TableProtoDeserializer.cpp
@@ -234,6 +234,8 @@
const pb::Attribute& pbAttr = pbCompoundValue.attr();
std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak);
attr->typeMask = pbAttr.format_flags();
+ attr->minInt = pbAttr.min_int();
+ attr->maxInt = pbAttr.max_int();
for (const pb::Attribute_Symbol& pbSymbol : pbAttr.symbols()) {
Attribute::Symbol symbol;
deserializeItemCommon(pbSymbol, &symbol.symbol);