Merge \\"Include cause when reporting errors.\\" into nyc-dev am: 60585e6c54
am: be8b6e6164
Change-Id: Id636e7d4e803aafc1aae768e09e9d79cba7522ed
diff --git a/Android.mk b/Android.mk
index 3410765..b08260a 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..ea640aa 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 = 16844082; // 0x1010532
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844081; // 0x1010531
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 = 16844080; // 0x1010530
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,20 @@
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 shortcutCategories = 16844077; // 0x101052d
+ field public static final int shortcutDisabledMessage = 16844076; // 0x101052c
+ field public static final int shortcutIcon = 16844073; // 0x1010529
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutIntentAction = 16844078; // 0x101052e
+ field public static final int shortcutIntentData = 16844079; // 0x101052f
+ field public static final int shortcutLongLabel = 16844075; // 0x101052b
+ field public static final int shortcutShortLabel = 16844074; // 0x101052a
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 = 16844083; // 0x1010533
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 +5049,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 +5770,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 +6501,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 +8210,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 +9520,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 +9548,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 +10062,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.String);
+ 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.String);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String);
+ }
+
+ 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 +30751,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 +30774,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 +32394,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 +33098,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 +36051,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 +36072,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 +36083,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 +36114,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 +36134,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 +36187,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 +36200,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 +36209,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 +36246,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 +36257,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 +36282,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 +36299,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 +36317,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 +36408,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 +36446,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 +36569,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 +36588,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 +36600,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 +36617,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 +36715,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 +37356,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 +37406,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 +40272,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 +41484,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
@@ -44527,6 +44711,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44621,6 +44806,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;
@@ -44692,6 +44878,7 @@
method public abstract java.lang.CharSequence getSelectedText(int);
method public abstract java.lang.CharSequence getTextAfterCursor(int, int);
method public abstract java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean performContextMenuAction(int);
method public abstract boolean performEditorAction(int);
method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44725,6 +44912,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44737,6 +44925,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..872c8fd 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 = 16844082; // 0x1010532
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844081; // 0x1010531
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 = 16844080; // 0x1010530
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,20 @@
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 shortcutCategories = 16844077; // 0x101052d
+ field public static final int shortcutDisabledMessage = 16844076; // 0x101052c
+ field public static final int shortcutIcon = 16844073; // 0x1010529
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutIntentAction = 16844078; // 0x101052e
+ field public static final int shortcutIntentData = 16844079; // 0x101052f
+ field public static final int shortcutLongLabel = 16844075; // 0x101052b
+ field public static final int shortcutShortLabel = 16844074; // 0x101052a
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 = 16844083; // 0x1010533
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 +5197,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 +5918,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 +6783,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 +8533,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 +9874,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 +9902,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 +10486,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.String);
+ 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.String);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String);
+ }
+
+ 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 +31654,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 +33324,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 +33347,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 +35099,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 +35806,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 +38887,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 +38910,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 +38921,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 +38952,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 +38972,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 +39031,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 +39046,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 +39055,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 +39094,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 +39106,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 +39131,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 +39148,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 +39166,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 +39257,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 +39296,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 +39334,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 +39364,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 +39543,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 +39562,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 +39574,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 +39592,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 +39647,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 +39691,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 +39752,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 +40395,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 +40464,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 +43376,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 +44588,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
@@ -47529,6 +47818,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -47623,6 +47913,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;
@@ -47694,6 +47985,7 @@
method public abstract java.lang.CharSequence getSelectedText(int);
method public abstract java.lang.CharSequence getTextAfterCursor(int, int);
method public abstract java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean performContextMenuAction(int);
method public abstract boolean performEditorAction(int);
method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -47727,6 +48019,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -47739,6 +48032,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..dcdcf9d 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 = 16844082; // 0x1010532
field public static final int contextPopupMenuStyle = 16844033; // 0x1010501
+ field public static final int contextUri = 16844081; // 0x1010531
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 = 16844080; // 0x1010530
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,20 @@
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 shortcutCategories = 16844077; // 0x101052d
+ field public static final int shortcutDisabledMessage = 16844076; // 0x101052c
+ field public static final int shortcutIcon = 16844073; // 0x1010529
+ field public static final int shortcutId = 16844072; // 0x1010528
+ field public static final int shortcutIntentAction = 16844078; // 0x101052e
+ field public static final int shortcutIntentData = 16844079; // 0x101052f
+ field public static final int shortcutLongLabel = 16844075; // 0x101052b
+ field public static final int shortcutShortLabel = 16844074; // 0x101052a
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 = 16844083; // 0x1010533
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 +5050,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 +5776,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 +6507,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 +8218,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 +9532,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 +9560,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 +10075,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.String);
+ 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.String);
+ method public android.content.pm.ShortcutInfo.Builder setRank(int);
+ method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String);
+ }
+
+ 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 +30825,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 +30848,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 +32468,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 +33175,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 +36129,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 +36150,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 +36161,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 +36192,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 +36212,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 +36265,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 +36278,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 +36287,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 +36324,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 +36335,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 +36360,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 +36377,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 +36395,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 +36486,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 +36524,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 +36647,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 +36666,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 +36678,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 +36695,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 +36793,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 +37434,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 +37484,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 +40352,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 +41564,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
@@ -44606,6 +44791,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44700,6 +44886,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;
@@ -44771,6 +44958,7 @@
method public abstract java.lang.CharSequence getSelectedText(int);
method public abstract java.lang.CharSequence getTextAfterCursor(int, int);
method public abstract java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public abstract boolean performContextMenuAction(int);
method public abstract boolean performEditorAction(int);
method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44804,6 +44992,7 @@
method public java.lang.CharSequence getSelectedText(int);
method public java.lang.CharSequence getTextAfterCursor(int, int);
method public java.lang.CharSequence getTextBeforeCursor(int, int);
+ method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle);
method public boolean performContextMenuAction(int);
method public boolean performEditorAction(int);
method public boolean performPrivateCommand(java.lang.String, android.os.Bundle);
@@ -44816,6 +45005,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/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 9118f01..df2c6fc 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..2b9cfa0 100644
--- a/core/java/android/content/pm/EphemeralResolveInfo.java
+++ b/core/java/android/content/pm/EphemeralResolveInfo.java
@@ -37,10 +37,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 +52,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 +79,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 +86,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 +116,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();
+ 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..6cb50fc 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;
@@ -166,8 +174,6 @@
* @param shortcuts all shortcuts from the package (dynamic 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 +182,6 @@
/**
* Represents a query passed to {@link #getShortcuts(ShortcutQuery, UserHandle)}.
- *
- * @hide
*/
public static class ShortcutQuery {
/**
@@ -191,6 +195,11 @@
public static final int FLAG_GET_PINNED = 1 << 1;
/**
+ * Include manifest shortcuts in the result.
+ */
+ public static final int FLAG_GET_MANIFEST = 1 << 3;
+
+ /**
* Requests "key" fields only. See {@link ShortcutInfo#hasKeyFieldsOnly()} for which
* fields are available.
*/
@@ -201,6 +210,7 @@
value = {
FLAG_GET_DYNAMIC,
FLAG_GET_PINNED,
+ FLAG_GET_MANIFEST,
FLAG_GET_KEY_FIELDS_ONLY,
})
@Retention(RetentionPolicy.SOURCE)
@@ -227,37 +237,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 +443,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 +462,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,
@@ -488,8 +501,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) {
@@ -529,12 +540,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 +557,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 +574,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 +643,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 +662,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..896fa43 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -22,6 +22,8 @@
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;
@@ -29,7 +31,9 @@
import android.os.PersistableBundle;
import android.os.UserHandle;
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 +54,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 +104,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 +139,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 +157,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 +219,9 @@
private int mFlags;
// Internal use only.
- private int mIconResourceId;
+ private int mIconResId;
+
+ private String mIconResName;
// Internal use only.
@Nullable
@@ -175,10 +237,14 @@
// 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;
+ mTextResId = b.mTextResId;
+ mDisabledMessage = b.mDisabledMessage;
+ mDisabledMessageResId = b.mDisabledMessageResId;
mCategories = clone(b.mCategories);
mIntent = b.mIntent;
if (mIntent != null) {
@@ -188,7 +254,7 @@
mIntentPersistableExtras = new PersistableBundle(intentExtras);
}
}
- mWeight = b.mWeight;
+ mRank = b.mRank;
mExtras = b.mExtras;
updateTimestamp();
}
@@ -204,7 +270,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");
}
@@ -219,10 +288,10 @@
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;
+ mActivity = source.mActivity;
if ((cloneFlags & CLONE_REMOVE_ICON) == 0) {
mIcon = source.mIcon;
@@ -230,14 +299,25 @@
}
mTitle = source.mTitle;
+ mTitleResId = source.mTitleResId;
mText = source.mText;
+ mTextResId = source.mTextResId;
+ mDisabledMessage = source.mDisabledMessage;
+ mDisabledMessageResId = source.mDisabledMessageResId;
mCategories = clone(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 +325,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,33 +548,70 @@
}
/**
+ * @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);
@@ -288,14 +620,12 @@
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 +640,6 @@
throw getInvalidIconException();
}
if (icon.hasTint()) {
- // TODO support it
throw new IllegalArgumentException("Icons with tints are not supported");
}
@@ -331,19 +660,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 +710,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 +736,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 String 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 String 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 String 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 String 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 String disabledMessage) {
+ Preconditions.checkState(
+ mDisabledMessageResId == 0, "disabledMessageResId already set");
+ mDisabledMessage =
+ Preconditions.checkStringNotEmpty(disabledMessage, "disabledMessage");
return this;
}
@@ -441,16 +840,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 +887,7 @@
* Return the package name of the creator application.
*/
@NonNull
- public String getPackageName() {
+ public String getPackage() {
return mPackageName;
}
@@ -496,11 +898,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 +919,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 +1032,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 +1147,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 +1217,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 +1241,7 @@
* following fields are available.
* <ul>
* <li>{@link #getId()}
- * <li>{@link #getPackageName()}
+ * <li>{@link #getPackage()}
* <li>{@link #getLastChangedTimestamp()}
* <li>{@link #isDynamic()}
* <li>{@link #isPinned()}
@@ -690,6 +1253,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 +1276,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 +1299,90 @@
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;
+ }
+
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);
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();
+ mIconResId = 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 +1399,29 @@
dest.writeInt(mUserId);
dest.writeString(mId);
dest.writeString(mPackageName);
- dest.writeParcelable(mActivityComponent, flags);
+ dest.writeParcelable(mActivity, flags);
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.writeInt(mIconResId);
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 +1468,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 +1536,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 +1551,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 +1569,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;
+ mTextResId = textResId;
+ mTextResName = textResName;
+ mDisabledMessage = disabledMessage;
+ mDisabledMessageResId = disabledMessageResId;
+ mDisabledMessageResName = disabledMessageResName;
mCategories = clone(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/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/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..fbd61cf3 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 3d37256..5853503 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
/**
@@ -6227,6 +6238,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
@@ -8549,6 +8583,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
@@ -8584,6 +8636,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.
*
@@ -8982,12 +9043,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 19b1cf3..65f0caa 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..7c3fc8b 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 insertContent(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..7ed0d27 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#insertContent(InputContentInfo, Bundle)}.
+ *
+ * <p>{@code null} or an empty array means that
+ * {@link InputConnection#insertContent(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..05e0569 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 insert 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 insertContent(@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..fb24fcd 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.INSERT_CONTENT,
})
public @interface MissingMethodFlags {
/**
@@ -78,6 +81,11 @@
* {@link android.os.Build.VERSION_CODES#N} and later.
*/
int CLOSE_CONNECTION = 1 << 6;
+ /**
+ * {@link InputConnection#insertContent(InputContentInfo, Bundle)} is available in
+ * {@link android.os.Build.VERSION_CODES#N} MR-1 and later.
+ */
+ int INSERT_CONTENT = 1 << 7;
}
private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
@@ -127,6 +135,9 @@
if (!hasCloseConnection(clazz)) {
flags |= MissingMethodFlags.CLOSE_CONNECTION;
}
+ if (!hasInsertContent(clazz)) {
+ flags |= MissingMethodFlags.INSERT_CONTENT;
+ }
sMissingMethodsMap.put(clazz, flags);
return flags;
}
@@ -195,6 +206,16 @@
}
}
+ private static boolean hasInsertContent(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("insertContent", 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.INSERT_CONTENT) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("InsertContent(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..431836a 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 insertContent(InputContentInfo inputContentInfo, Bundle opts) {
+ return mTarget.insertContent(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..fb532e2 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -19,6 +19,7 @@
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
+import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
@@ -75,6 +76,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 +5984,11 @@
public void closeConnection() {
getTarget().closeConnection();
}
+
+ @Override
+ public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) {
+ return getTarget().insertContent(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/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/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..fc189a3 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_INSERT_CONTENT = 160;
@GuardedBy("mLock")
@Nullable
@@ -241,6 +243,11 @@
dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION));
}
+ public void insertContent(InputContentInfo inputContentInfo, Bundle opts,
+ int seq, IInputContextCallback callback) {
+ dispatchMessage(obtainMessageOOSC(DO_INSERT_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_INSERT_CONTENT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ InputConnection ic = getInputConnection();
+ if (ic == null || !isActive()) {
+ Log.w(TAG, "insertContent on inactive InputConnection");
+ args.callback.setInsertContentResult(false, args.seq);
+ return;
+ }
+ final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1;
+ if (inputContentInfo == null || !inputContentInfo.validate()) {
+ Log.w(TAG, "insertContent with invalid inputContentInfo="
+ + inputContentInfo);
+ args.callback.setInsertContentResult(false, args.seq);
+ return;
+ }
+ args.callback.setInsertContentResult(
+ ic.insertContent(inputContentInfo, (Bundle) args.arg2), args.seq);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Got RemoteException calling insertContent", 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..bd47355 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 insertContent(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..933cdc0 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 setInsertContentResult(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..6d4d45c 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -16,6 +16,7 @@
package com.android.internal.view;
+import android.content.ClipData;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
@@ -29,6 +30,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 +48,8 @@
public ExtractedText mExtractedText;
public int mCursorCapsMode;
public boolean mRequestUpdateCursorAnchorInfoResult;
-
+ public boolean mInsertContentResult;
+
// 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 +175,19 @@
}
}
+ public void setInsertContentResult(boolean result, int seq) {
+ synchronized (this) {
+ if (seq == mSeq) {
+ mInsertContentResult = result;
+ mHaveValue = true;
+ notifyAll();
+ } else {
+ Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ + ") in setInsertContentResult, ignoring.");
+ }
+ }
+ }
+
/**
* Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
*
@@ -491,6 +507,28 @@
// Nothing should happen when called from input method.
}
+ public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) {
+ boolean result = false;
+ if (isMethodMissing(MissingMethodFlags.INSERT_CONTENT)) {
+ // This method is not implemented.
+ return false;
+ }
+ try {
+ InputContextCallback callback = InputContextCallback.getInstance();
+ mIInputContext.insertContent(inputContentInfo, opts, callback.mSeq, callback);
+ synchronized (callback) {
+ callback.waitForResultLocked();
+ if (callback.mHaveValue) {
+ result = callback.mInsertContentResult;
+ }
+ }
+ 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/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..d0c6a8e 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,16 @@
color. -->
<attr name="colorBackground" />
</declare-styleable>
+
+ <declare-styleable name="Shortcut">
+ <attr name="shortcutId" format="string" />
+ <attr name="enabled" />
+ <attr name="shortcutIcon" format="reference" />
+ <attr name="shortcutShortLabel" format="reference" />
+ <attr name="shortcutLongLabel" format="reference" />
+ <attr name="shortcutDisabledMessage" format="reference" />
+ <attr name="shortcutCategories" format="string" />
+ <attr name="shortcutIntentAction" format="string" />
+ <attr name="shortcutIntentData" format="string" />
+ </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/config.xml b/core/res/res/values/config.xml
index 87cb2e8..78e6ed8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2489,4 +2489,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..da31767 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2728,4 +2728,22 @@
<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="shortcutIcon" />
+ <public type="attr" name="shortcutShortLabel" />
+ <public type="attr" name="shortcutLongLabel" />
+ <public type="attr" name="shortcutDisabledMessage" />
+ <public type="attr" name="shortcutCategories" />
+ <public type="attr" name="shortcutIntentAction" />
+ <public type="attr" name="shortcutIntentData" />
+ <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 48744b6..5db562d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4366,6 +4366,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 341a0a4c..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 7e9b57c..6e937ff 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" />
@@ -2609,4 +2615,12 @@
<java-symbol type="array" name="config_defaultPinnerServiceFiles" />
<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" />
</resources>
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 11bb106..ea1b626 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -551,4 +551,6 @@
<item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
</style>
+ <!-- DeviceDefault theme for the default system theme. -->
+ <style name="Theme.DeviceDefault.System" parent="Theme.Material.Light.DarkActionBar" />
</resources>
diff --git a/core/res/res/values/themes_material.xml b/core/res/res/values/themes_material.xml
index 2ea5c5e..881b9b3 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>
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..6bb93aec 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;
}
@@ -390,9 +392,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 +419,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 +516,8 @@
@Config int mChangingConfigurations;
VectorDrawable mVectorDrawable;
+ private final boolean mShouldIgnoreInvalidAnim;
+
/** Animators that require a theme before inflation. */
ArrayList<PendingAnimator> mPendingAnims;
@@ -473,6 +529,7 @@
public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
Callback owner, Resources res) {
+ mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
if (copy != null) {
mChangingConfigurations = copy.mChangingConfigurations;
@@ -616,8 +673,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 +1041,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 +1054,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 +1076,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 +1139,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 +1148,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 +1183,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 +1211,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 +1220,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 +1245,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 +1309,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 +1321,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 +1356,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 +1390,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;
}
@@ -1483,6 +1593,7 @@
}
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 +1609,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 be2dab9..5a3300a 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..76ba0a7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
@@ -156,7 +156,7 @@
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);
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 2c874e5..109d2c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -104,8 +104,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() {
@@ -363,7 +361,7 @@
}
- private void showEdit(final View v) {
+ public void showEdit(final View v) {
v.post(new Runnable() {
@Override
public void run() {
@@ -371,8 +369,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 c984abe..afc987e2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -141,12 +141,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;
@@ -156,25 +155,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
@@ -188,11 +168,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);
}
@@ -217,8 +197,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 2b59c68..a2ad46a 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
@@ -241,6 +244,7 @@
@Override
public void run() {
mVpnVisible = mSC.isVpnEnabled();
+ mVpnIconId = currentVpnIconId(mSC.isVpnBranded());
apply();
}
});
@@ -428,6 +432,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) {
@@ -556,6 +569,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;
@@ -677,4 +694,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 68e5d0b..ae1e5d0 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.text.TextUtils;
@@ -320,6 +321,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..5527a41 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2212,12 +2212,118 @@
// 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;
+
+ // ---- 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 4e0ddd6..04a0990 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -29,6 +29,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;
@@ -70,10 +71,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;
@@ -732,8 +735,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) {
@@ -753,18 +756,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);
}
}
@@ -1804,6 +1808,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) {
@@ -1813,6 +1826,7 @@
SomeArgs args = SomeArgs.obtain();
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
+ args.arg3 = requestTime;
args.argi1 = widget.appWidgetId;
args.argi2 = viewId;
@@ -1823,9 +1837,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.
@@ -1874,7 +1889,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) {
@@ -1907,6 +1922,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;
@@ -1916,6 +1937,7 @@
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
args.arg3 = widget.provider.info;
+ args.arg4 = requestTime;
args.argi1 = widget.appWidgetId;
mCallbackHandler.obtainMessage(
@@ -1924,9 +1946,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);
@@ -3410,10 +3433,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: {
@@ -3429,11 +3453,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;
}
}
@@ -3772,20 +3798,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
@@ -3850,6 +3897,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
@@ -3858,7 +3909,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 9154b8e..71964d1 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 09f24ed..2c7ef7e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18730,8 +18730,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.
@@ -18740,9 +18741,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
@@ -21207,6 +21209,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 7a43d53..f78fa8d 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;
}
@@ -2741,6 +2743,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) {
@@ -4408,6 +4413,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 522e42b..793f411 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/pm/EphemeralResolverConnection.java b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
index fe6fb1f..b25ef175 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..43a0b91 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));
}
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 2986a12..2c4d732 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 = 0xFFFFFFFF;
+ private static int DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT = 5;
+
/** Permission grant: not grant the permission. */
private static final int GRANT_DENIED = 1;
@@ -2944,17 +2949,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) {
@@ -2973,7 +2981,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);
@@ -2998,9 +3006,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) {
@@ -4989,48 +5000,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; 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..ace14ac 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,124 @@
}
/**
- * 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();
+ }
+
if (changed) {
// This will send a notification to the launcher, and also save .
s.packageShortcutsChanged(getPackageName(), getPackageUserId());
@@ -483,9 +689,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();
+ }
+ 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());
+ }
+ }
+ service.verifyStates();
+
+ 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 +1083,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 +1099,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 +1130,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 +1146,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 +1167,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 +1190,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 +1224,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 +1266,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 +1289,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 +1346,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..ec19927
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -0,0 +1,253 @@
+/*
+ * 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.net.Uri;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+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.pm.Shortcuts";
+
+ private static final String TAG_SHORTCUTS = "shortcuts";
+ private static final String TAG_SHORTCUT = "shortcut";
+
+ @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;
+
+ if (pi != null && pi.activities != null) {
+ for (ActivityInfo activityInfo : pi.activities) {
+ result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result);
+ }
+ }
+ 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;
+
+ outer:
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+ final int depth = parser.getDepth();
+ final String tag = parser.getName();
+
+ 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 (ShortcutService.DEBUG) {
+ Slog.d(TAG, "Shortcut=" + si);
+ }
+ if (result != null) {
+ for (int i = result.size() - 1; i >= 0; i--) {
+ if (si.getId().equals(result.get(i).getId())) {
+ Slog.w(TAG, "Duplicate shortcut ID detected, skipping.");
+ continue outer;
+ }
+ }
+ }
+
+ if (si != null) {
+ if (numShortcuts >= maxShortcuts) {
+ Slog.w(TAG, "More than " + maxShortcuts + " shortcuts found for "
+ + activityInfo.getComponentName() + ", ignoring the rest.");
+ return result;
+ }
+
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+ result.add(si);
+ numShortcuts++;
+ rank++;
+ }
+ continue;
+ }
+ Slog.w(TAG, "Unknown tag " + tag);
+ }
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ return result;
+ }
+
+ 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_shortcutIcon, 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);
+ final String categories = sa.getString(R.styleable.Shortcut_shortcutCategories);
+ String intentAction = sa.getString(R.styleable.Shortcut_shortcutIntentAction);
+ final String intentData = sa.getString(R.styleable.Shortcut_shortcutIntentData);
+
+ 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;
+ }
+ if (TextUtils.isEmpty(intentAction)) {
+ if (enabled) {
+ Slog.w(TAG, "Shortcut intent action must be provided. activity=" + activity);
+ return null;
+ } else {
+ // Disabled shortcut doesn't have to have an action, but just set VIEW as the
+ // default.
+ intentAction = Intent.ACTION_VIEW;
+ }
+ }
+
+ final ArraySet<String> categoriesSet;
+ if (categories == null) {
+ categoriesSet = null;
+ } else {
+ final String[] arr = categories.split(":");
+ categoriesSet = new ArraySet<>(arr.length);
+ for (String v : arr) {
+ categoriesSet.add(v);
+ }
+ }
+ final Intent intent = new Intent(intentAction);
+ if (!TextUtils.isEmpty(intentData)) {
+ intent.setData(Uri.parse(intentData));
+ }
+
+ return createShortcutFromManifest(
+ service,
+ userId,
+ id,
+ packageName,
+ activity,
+ titleResId,
+ textResId,
+ disabledMessageResId,
+ categoriesSet,
+ intent,
+ 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, Set<String> categories,
+ Intent intent, 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
+ categories,
+ 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..1619168 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,35 @@
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.
- for (int i = 0; i < size; i++) {
- fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
- }
+ // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
+ ps.clearAllImplicitRanks();
+ assignImplicitRanks(newShortcuts);
// 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 +1540,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 +1620,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 +1638,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 +1707,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 +1729,11 @@
verifyCaller(packageName, userId);
synchronized (mLock) {
- getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
+ getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts();
}
packageShortcutsChanged(packageName, userId);
+
+ verifyStates();
}
@Override
@@ -1499,6 +1748,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 +1774,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 +1793,7 @@
synchronized (mLock) {
return mMaxUpdatesPerInterval
- - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
+ - getPackageShortcutsLocked(packageName, userId).getApiCallCount();
}
}
@@ -1547,7 +1807,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 +1815,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 +1896,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 +1915,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 +1955,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 +1989,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 +2002,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 +2028,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 +2063,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 +2077,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 +2107,7 @@
synchronized (mLock) {
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
- .attemptToRestoreIfNeededAndSave(ShortcutService.this);
+ .attemptToRestoreIfNeededAndSave();
final ShortcutInfo si = getShortcutInfoLocked(
launcherUserId, callingPackage, packageName, shortcutId, userId);
@@ -1819,9 +2121,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 +2145,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 +2164,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 +2193,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 +2217,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 +2256,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 +2268,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 +2331,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 +2377,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 +2390,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 +2406,8 @@
packageUserId));
}
cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+
+ verifyStates();
}
private void handlePackageDataCleared(String packageName, int packageUserId) {
@@ -2064,14 +2416,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 +2446,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 +2469,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 +2487,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 +2558,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 +2603,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 +2718,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 +2940,7 @@
private void clearLauncher() {
synchronized (mLock) {
- getUserShortcutsLocked(mUserId).setLauncherComponent(
- ShortcutService.this, null);
+ getUserShortcutsLocked(mUserId).setDefaultLauncherComponent(null);
}
}
@@ -2510,7 +2950,7 @@
hasShortcutHostPermissionInner("-", mUserId);
getOutPrintWriter().println("Launcher: "
- + getUserShortcutsLocked(mUserId).getLauncherComponent());
+ + getUserShortcutsLocked(mUserId).getDefaultLauncherComponent());
}
}
@@ -2663,15 +3103,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 a85064b..0bf0cac 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);
@@ -2160,6 +2165,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;
@@ -2177,8 +2183,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;
}
@@ -2204,7 +2210,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;
@@ -2855,6 +2862,8 @@
mPm.onBeforeUserStartUninitialized(userId);
}
}
+
+ maybeInitializeDemoMode(userId);
}
/**
@@ -2869,6 +2878,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) {
@@ -2886,6 +2896,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/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..f6d54fc
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_1.xml
@@ -0,0 +1,13 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="data1"
+ />
+</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..bf14f49
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_1_alt.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1-alt"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="data1"
+ />
+</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..81a84b4
--- /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:shortcutIcon="@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..96ed382
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_2.xml
@@ -0,0 +1,39 @@
+<?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:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="http://a.b.c/"
+ />
+ <shortcut
+ android:shortcutId="ms2"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ android:shortcutCategories="android.shortcut.conversation"
+ android:shortcutIntentAction="action2"
+ android:shortcutIntentData="http://a.b.c/2"
+ />
+</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..2f814b7
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="action1"
+ />
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutIntentAction="action2"
+ />
+</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..56dba0e
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5.xml
@@ -0,0 +1,53 @@
+<?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:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="http://a.b.c/1"
+ />
+ <shortcut
+ android:shortcutId="ms2"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ android:shortcutCategories="android.shortcut.conversation"
+ android:shortcutIntentAction="action2"
+ />
+ <shortcut
+ android:shortcutId="ms3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms4"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms5"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..74085d9
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5_alt.xml
@@ -0,0 +1,53 @@
+<?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:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="http://a.b.c/1"
+ />
+ <shortcut
+ android:shortcutId="ms2_alt"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ android:shortcutCategories="android.shortcut.conversation"
+ android:shortcutIntentAction="action2"
+ />
+ <shortcut
+ android:shortcutId="ms3_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms4_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms5_alt"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..3d6eb22
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml
@@ -0,0 +1,53 @@
+<?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:shortcutIcon="@drawable/icon1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutLongLabel="@string/shortcut_text1"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
+ android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
+ android:shortcutIntentAction="action1"
+ android:shortcutIntentData="http://a.b.c/1"
+ />
+ <shortcut
+ android:shortcutId="ms4"
+ android:enabled="true"
+ android:shortcutIcon="@drawable/icon2"
+ android:shortcutShortLabel="@string/shortcut_title2"
+ android:shortcutLongLabel="@string/shortcut_text2"
+ android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
+ android:shortcutCategories="android.shortcut.conversation"
+ android:shortcutIntentAction="action2"
+ />
+ <shortcut
+ android:shortcutId="ms3"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="ms1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..5822496
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_1.xml
@@ -0,0 +1,26 @@
+<?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"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="x1"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..ca67ec7
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_2.xml
@@ -0,0 +1,26 @@
+<?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:shortcutIntentAction="android.intent.action.VIEW"
+ />
+ <shortcut
+ android:shortcutId="x2"
+ android:shortcutShortLabel="@string/shortcut_title1"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..fb7b31c
--- /dev/null
+++ b/services/tests/servicestests/res/xml/shortcut_error_3.xml
@@ -0,0 +1,26 @@
+<?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"
+ android:shortcutIntentAction="android.intent.action.VIEW"
+ />
+</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..69152d4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -0,0 +1,1587 @@
+/*
+ * 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_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED);
+ }
+
+ @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_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED);
+ 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 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..de06047
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -0,0 +1,5821 @@
+/*
+ * 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_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 {
+ 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)
+ );
+ assertShortcutIds(assertAllDynamic(shortcuts.getValue()),
+ "s1", "s2", "s3");
+
+ // 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)
+ );
+ assertShortcutIds(assertAllDynamic(shortcuts.getValue()),
+ "s1", "s2", "s3");
+
+ // 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)
+ );
+ assertShortcutIds(assertAllDynamic(shortcuts.getValue()),
+ "s1", "s2", "s3", "s4");
+
+ // 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)
+ );
+ assertShortcutIds(assertAllDynamic(shortcuts.getValue()),
+ "s2", "s3", "s4");
+
+ // 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)
+ );
+ assertShortcutIds(assertAllDynamic(shortcuts.getValue()),
+ "s2", "s3", "s4");
+
+ // 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)
+ );
+ assertEquals(0, shortcuts.getValue().size());
+
+ // 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)
+ );
+ assertEquals(0, shortcuts.getValue().size());
+ }
+
+ 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_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, () -> {
+ assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+ mManager.getManifestShortcuts()))),
+ "ms1", "ms2", "ms3", "ms4", "ms5");
+
+ // check first shortcut.
+ ShortcutInfo si = getCallerShortcut("ms1");
+
+ assertEquals("ms1", si.getId());
+ 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());
+
+ // check another
+ si = getCallerShortcut("ms2");
+
+ 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());
+
+ // check another
+ si = getCallerShortcut("ms3");
+
+ assertEquals("ms3", si.getId());
+ 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());
+
+ assertEquals(null, si.getCategories());
+ assertEquals("android.intent.action.VIEW", si.getIntent().getAction());
+ assertEquals(null, si.getIntent().getData());
+ });
+ }
+
+ 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..428b872
--- /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(null, 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(null, 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..7ba4c68
--- /dev/null
+++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
@@ -0,0 +1,721 @@
+/*
+ * 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.Log;
+
+import junit.framework.Assert;
+
+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.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> List<T> assertEmpty(List<T> list) {
+ assertEquals(0, list.size());
+ return list;
+ }
+
+ 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 List<ShortcutInfo> mList;
+
+ ShortcutListAsserter(List<ShortcutInfo> list) {
+ mList = new ArrayList<>(list);
+ }
+
+ public ShortcutListAsserter selectDynamic() {
+ return new ShortcutListAsserter(
+ filter(mList, ShortcutInfo::isDynamic));
+ }
+
+ public ShortcutListAsserter selectManifest() {
+ return new ShortcutListAsserter(
+ filter(mList, ShortcutInfo::isManifestShortcut));
+ }
+
+ public ShortcutListAsserter selectPinned() {
+ return new ShortcutListAsserter(
+ filter(mList, ShortcutInfo::isPinned));
+ }
+
+ public ShortcutListAsserter selectByActivity(ComponentName activity) {
+ return new ShortcutListAsserter(
+ ShortcutManagerTestUtils.filterByActivity(mList, activity));
+ }
+
+ public ShortcutListAsserter selectByChangedSince(long time) {
+ return new ShortcutListAsserter(
+ ShortcutManagerTestUtils.changedSince(mList, time));
+ }
+
+ public ShortcutListAsserter toSortByRank() {
+ return new ShortcutListAsserter(
+ ShortcutManagerTestUtils.sortedByRank(mList));
+ }
+
+ 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++) {
+ assertEquals("Rank not sequential", i, mList.get(i).getRank());
+ }
+ return this;
+ }
+
+ public ShortcutListAsserter haveRanksInOrder(String... expectedIds) {
+ toSortByRank()
+ .haveSequentialRanks()
+ .haveIdsOrdered(expectedIds);
+ return this;
+ }
+
+ public ShortcutListAsserter isEmpty() {
+ assertEquals(0, mList.size());
+ 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..0dcd1521 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 b5cf212e..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..9d9ce72
--- /dev/null
+++ b/telephony/java/android/telephony/TelephonyHistogram.java
@@ -0,0 +1,293 @@
+/*
+ * 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 category;
+
+ // Unique Id identifying a sample within particular category of histogram
+ private final int id;
+
+ // Min time taken in ms
+ private int minTimeMs;
+
+ // Max time taken in ms
+ private int maxTimeMs;
+
+ // Average time taken in ms
+ private int averageTimeMs;
+
+ // Total count of samples
+ private int sampleCount;
+
+ // Array storing time taken for first #RANGE_CALCULATION_COUNT samples of histogram.
+ private int[] initialTimings;
+
+ // Total number of time ranges expected (must be greater than 1)
+ private final int bucketCount;
+
+ // Array storing endpoints of range buckets. Calculated based on values of minTime & maxTime
+ // after totalTimeCount is #RANGE_CALCULATION_COUNT.
+ private final int[] bucketEndPoints;
+
+ // Array storing counts for each time range starting from smallest value range
+ private final int[] bucketCounters;
+
+ /**
+ * 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");
+ }
+ this.category = category;
+ this.id = id;
+ this.minTimeMs = Integer.MAX_VALUE;
+ this.maxTimeMs = 0;
+ this.averageTimeMs = 0;
+ this.sampleCount = 0;
+ initialTimings = new int[RANGE_CALCULATION_COUNT];
+ this.bucketCount = bucketCount;
+ bucketEndPoints = new int[bucketCount - 1];
+ bucketCounters = new int[bucketCount];
+ }
+
+ public TelephonyHistogram(TelephonyHistogram th) {
+ category = th.getCategory();
+ id = th.getId();
+ minTimeMs = th.getMinTime();
+ maxTimeMs = th.getMaxTime();
+ averageTimeMs = th.getAverageTime();
+ sampleCount = th.getSampleCount();
+ initialTimings = th.getInitialTimings();
+ bucketCount = th.getBucketCount();
+ bucketEndPoints = th.getBucketEndPoints();
+ bucketCounters = th.getBucketCounters();
+ }
+
+ public int getCategory() {
+ return category;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public int getMinTime() {
+ return minTimeMs;
+ }
+
+ public int getMaxTime() {
+ return maxTimeMs;
+ }
+
+ public int getAverageTime() {
+ return averageTimeMs;
+ }
+
+ public int getSampleCount () {
+ return sampleCount;
+ }
+
+ private int[] getInitialTimings() {
+ return initialTimings;
+ }
+
+ public int getBucketCount() {
+ return bucketCount;
+ }
+
+ public int[] getBucketEndPoints() {
+ return getDeepCopyOfArray(bucketEndPoints);
+ }
+
+ public int[] getBucketCounters() {
+ return getDeepCopyOfArray(bucketCounters);
+ }
+
+ 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 time) {
+ int i;
+ for (i = 0; i < bucketEndPoints.length; i++) {
+ if (time <= bucketEndPoints[i]) {
+ bucketCounters[i]++;
+ return;
+ }
+ }
+ bucketCounters[i]++;
+ }
+
+ // 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 (sampleCount == 0 || (sampleCount == Integer.MAX_VALUE)) {
+ if (sampleCount == 0) {
+ minTimeMs = time;
+ maxTimeMs = time;
+ averageTimeMs = time;
+ } else {
+ initialTimings = new int[RANGE_CALCULATION_COUNT];
+ }
+ sampleCount = 1;
+ Arrays.fill(initialTimings, 0);
+ initialTimings[0] = time;
+ Arrays.fill(bucketEndPoints, 0);
+ Arrays.fill(bucketCounters, 0);
+ } else {
+ if (time < minTimeMs) {
+ minTimeMs = time;
+ }
+ if (time > maxTimeMs) {
+ maxTimeMs = time;
+ }
+ long totalTime = ((long)averageTimeMs) * sampleCount + time;
+ averageTimeMs = (int)(totalTime/++sampleCount);
+
+ if (sampleCount < RANGE_CALCULATION_COUNT) {
+ initialTimings[sampleCount - 1] = time;
+ } else if (sampleCount == RANGE_CALCULATION_COUNT) {
+ initialTimings[sampleCount - 1] = time;
+
+ // Calculate bucket endpoints based on bucketCount expected
+ for (int i = 1; i < bucketCount; i++) {
+ int endPt = minTimeMs + (i * (maxTimeMs - minTimeMs)) / bucketCount;
+ bucketEndPoints[i - 1] = endPt;
+ }
+
+ // Use values stored in initialTimings[] to update bucketCounters
+ for (int j = 0; j < RANGE_CALCULATION_COUNT; j++) {
+ addToBucketCounter(initialTimings[j]);
+ }
+ initialTimings = null;
+ } else {
+ addToBucketCounter(time);
+ }
+
+ }
+ }
+
+ public String toString() {
+ String basic = " Histogram id = " + id + " Time(ms): min = " + minTimeMs + " max = "
+ + maxTimeMs + " avg = " + averageTimeMs + " Count = " + sampleCount;
+ if (sampleCount < RANGE_CALCULATION_COUNT) {
+ return basic;
+ } else {
+ StringBuffer intervals = new StringBuffer(" Interval Endpoints:");
+ for (int i = 0; i < bucketEndPoints.length; i++) {
+ intervals.append(" " + bucketEndPoints[i]);
+ }
+ intervals.append(" Interval counters:");
+ for (int i = 0; i < bucketCounters.length; i++) {
+ intervals.append(" " + bucketCounters[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) {
+ category = in.readInt();
+ id = in.readInt();
+ minTimeMs = in.readInt();
+ maxTimeMs = in.readInt();
+ averageTimeMs = in.readInt();
+ sampleCount = in.readInt();
+ if (in.readInt() == PRESENT) {
+ initialTimings = new int[RANGE_CALCULATION_COUNT];
+ in.readIntArray(initialTimings);
+ }
+ bucketCount = in.readInt();
+ bucketEndPoints = new int[bucketCount - 1];
+ in.readIntArray(bucketEndPoints);
+ bucketCounters = new int[bucketCount];
+ in.readIntArray(bucketCounters);
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(category);
+ out.writeInt(id);
+ out.writeInt(minTimeMs);
+ out.writeInt(maxTimeMs);
+ out.writeInt(averageTimeMs);
+ out.writeInt(sampleCount);
+ if (initialTimings == null) {
+ out.writeInt(ABSENT);
+ } else {
+ out.writeInt(PRESENT);
+ out.writeIntArray(initialTimings);
+ }
+ out.writeInt(bucketCount);
+ out.writeIntArray(bucketEndPoints);
+ out.writeIntArray(bucketCounters);
+ }
+
+ @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/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 9f7d714b..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);