diff options
29 files changed, 1291 insertions, 127 deletions
diff --git a/Android.bp b/Android.bp index b0e0b35a1f76..7f4cd849ab77 100644 --- a/Android.bp +++ b/Android.bp @@ -722,6 +722,7 @@ filegroup { srcs: [ "core/java/android/annotation/StringDef.java", "core/java/android/net/annotations/PolicyDirection.java", + "core/java/com/android/internal/util/HexDump.java", "core/java/com/android/internal/util/IState.java", "core/java/com/android/internal/util/State.java", "core/java/com/android/internal/util/StateMachine.java", diff --git a/api/current.txt b/api/current.txt index ff74ce8b43dc..ea9da1513528 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9,7 +9,6 @@ package android { ctor public Manifest.permission(); field public static final String ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER"; field public static final String ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION"; - field public static final String ACCESS_CALL_AUDIO = "android.permission.ACCESS_CALL_AUDIO"; field public static final String ACCESS_CHECKIN_PROPERTIES = "android.permission.ACCESS_CHECKIN_PROPERTIES"; field public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; field public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; @@ -47774,7 +47773,7 @@ package android.telephony { method public String createAppSpecificSmsToken(android.app.PendingIntent); method @Nullable public String createAppSpecificSmsTokenWithPackageInfo(@Nullable String, @NonNull android.app.PendingIntent); method public java.util.ArrayList<java.lang.String> divideMessage(String); - method @Deprecated public void downloadMultimediaMessage(android.content.Context, String, android.net.Uri, android.os.Bundle, android.app.PendingIntent); + method public void downloadMultimediaMessage(android.content.Context, String, android.net.Uri, android.os.Bundle, android.app.PendingIntent); method @NonNull public android.os.Bundle getCarrierConfigValues(); method public static android.telephony.SmsManager getDefault(); method public static int getDefaultSmsSubscriptionId(); @@ -47784,7 +47783,7 @@ package android.telephony { method public int getSubscriptionId(); method public void injectSmsPdu(byte[], String, android.app.PendingIntent); method public void sendDataMessage(String, String, short, byte[], android.app.PendingIntent, android.app.PendingIntent); - method @Deprecated public void sendMultimediaMessage(android.content.Context, android.net.Uri, String, android.os.Bundle, android.app.PendingIntent); + method public void sendMultimediaMessage(android.content.Context, android.net.Uri, String, android.os.Bundle, android.app.PendingIntent); method public void sendMultipartTextMessage(String, String, java.util.ArrayList<java.lang.String>, java.util.ArrayList<android.app.PendingIntent>, java.util.ArrayList<android.app.PendingIntent>); method public void sendMultipartTextMessage(@NonNull String, @Nullable String, @NonNull java.util.List<java.lang.String>, @Nullable java.util.List<android.app.PendingIntent>, @Nullable java.util.List<android.app.PendingIntent>, long); method public void sendMultipartTextMessage(@NonNull String, @Nullable String, @NonNull java.util.List<java.lang.String>, @Nullable java.util.List<android.app.PendingIntent>, @Nullable java.util.List<android.app.PendingIntent>, @NonNull String, @Nullable String); diff --git a/api/system-current.txt b/api/system-current.txt index 7e25382f06d9..b9153d96a56a 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -368,7 +368,6 @@ package android.app { method @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void setUidMode(@NonNull String, int, int); field public static final String OPSTR_ACCEPT_HANDOVER = "android:accept_handover"; field public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility"; - field public static final String OPSTR_ACCESS_CALL_AUDIO = "android:access_call_audio"; field public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications"; field public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn"; field public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot"; @@ -2030,10 +2029,10 @@ package android.content.pm { } public static class PackageInstaller.Session implements java.io.Closeable { - method public void addFile(int, @NonNull String, long, @NonNull byte[], @Nullable byte[]); + method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void addFile(int, @NonNull String, long, @NonNull byte[], @Nullable byte[]); method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void commitTransferred(@NonNull android.content.IntentSender); - method @Nullable public android.content.pm.DataLoaderParams getDataLoaderParams(); - method public void removeFile(int, @NonNull String); + method @Nullable @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public android.content.pm.DataLoaderParams getDataLoaderParams(); + method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void removeFile(int, @NonNull String); } public static class PackageInstaller.SessionInfo implements android.os.Parcelable { @@ -2054,7 +2053,7 @@ package android.content.pm { public static class PackageInstaller.SessionParams implements android.os.Parcelable { method @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public void setAllocateAggressive(boolean); method @Deprecated public void setAllowDowngrade(boolean); - method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setDataLoaderParams(@NonNull android.content.pm.DataLoaderParams); + method @RequiresPermission(allOf={android.Manifest.permission.INSTALL_PACKAGES, "com.android.permission.USE_INSTALLER_V2"}) public void setDataLoaderParams(@NonNull android.content.pm.DataLoaderParams); method public void setDontKillApp(boolean); method public void setEnableRollback(boolean); method public void setEnableRollback(boolean, int); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index d4749bd8f330..a5dcefcf3ab7 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1088,8 +1088,9 @@ public class AppOpsManager { public static final int OP_ACTIVATE_PLATFORM_VPN = AppProtoEnums.APP_OP_ACTIVATE_PLATFORM_VPN; /** @hide */ public static final int OP_LOADER_USAGE_STATS = AppProtoEnums.APP_OP_LOADER_USAGE_STATS; - /** @hide Access telephony call audio */ - public static final int OP_ACCESS_CALL_AUDIO = AppProtoEnums.APP_OP_ACCESS_CALL_AUDIO; + + // App op deprecated/removed. + private static final int OP_DEPRECATED_1 = AppProtoEnums.APP_OP_DEPRECATED_1; /** @hide Auto-revoke app permissions if app is unused for an extended period */ public static final int OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED = @@ -1396,9 +1397,6 @@ public class AppOpsManager { @SystemApi public static final String OPSTR_MANAGE_EXTERNAL_STORAGE = "android:manage_external_storage"; - /** @hide Access telephony call audio */ - @SystemApi - public static final String OPSTR_ACCESS_CALL_AUDIO = "android:access_call_audio"; /** @hide Auto-revoke app permissions if app is unused for an extended period */ @SystemApi @@ -1498,7 +1496,6 @@ public class AppOpsManager { OP_MANAGE_EXTERNAL_STORAGE, OP_INTERACT_ACROSS_PROFILES, OP_LOADER_USAGE_STATS, - OP_ACCESS_CALL_AUDIO, OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, OP_AUTO_REVOKE_MANAGED_BY_INSTALLER, }; @@ -1608,7 +1605,7 @@ public class AppOpsManager { OP_INTERACT_ACROSS_PROFILES, //INTERACT_ACROSS_PROFILES OP_ACTIVATE_PLATFORM_VPN, // ACTIVATE_PLATFORM_VPN OP_LOADER_USAGE_STATS, // LOADER_USAGE_STATS - OP_ACCESS_CALL_AUDIO, // ACCESS_CALL_AUDIO + OP_DEPRECATED_1, // deprecated OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, //AUTO_REVOKE_PERMISSIONS_IF_UNUSED OP_AUTO_REVOKE_MANAGED_BY_INSTALLER, //OP_AUTO_REVOKE_MANAGED_BY_INSTALLER }; @@ -1713,7 +1710,7 @@ public class AppOpsManager { OPSTR_INTERACT_ACROSS_PROFILES, OPSTR_ACTIVATE_PLATFORM_VPN, OPSTR_LOADER_USAGE_STATS, - OPSTR_ACCESS_CALL_AUDIO, + "", // deprecated OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, OPSTR_AUTO_REVOKE_MANAGED_BY_INSTALLER, }; @@ -1819,7 +1816,7 @@ public class AppOpsManager { "INTERACT_ACROSS_PROFILES", "ACTIVATE_PLATFORM_VPN", "LOADER_USAGE_STATS", - "ACCESS_CALL_AUDIO", + "deprecated", "AUTO_REVOKE_PERMISSIONS_IF_UNUSED", "AUTO_REVOKE_MANAGED_BY_INSTALLER", }; @@ -1926,7 +1923,7 @@ public class AppOpsManager { android.Manifest.permission.INTERACT_ACROSS_PROFILES, null, // no permission for OP_ACTIVATE_PLATFORM_VPN android.Manifest.permission.LOADER_USAGE_STATS, - Manifest.permission.ACCESS_CALL_AUDIO, + null, // deprecated operation null, // no permission for OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED null, // no permission for OP_AUTO_REVOKE_MANAGED_BY_INSTALLER }; @@ -2033,7 +2030,7 @@ public class AppOpsManager { null, // INTERACT_ACROSS_PROFILES null, // ACTIVATE_PLATFORM_VPN null, // LOADER_USAGE_STATS - null, // ACCESS_CALL_AUDIO + null, // deprecated operation null, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED null, // AUTO_REVOKE_MANAGED_BY_INSTALLER }; @@ -2139,7 +2136,7 @@ public class AppOpsManager { null, // INTERACT_ACROSS_PROFILES null, // ACTIVATE_PLATFORM_VPN null, // LOADER_USAGE_STATS - null, // ACCESS_CALL_AUDIO + null, // deprecated operation null, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED null, // AUTO_REVOKE_MANAGED_BY_INSTALLER }; @@ -2244,7 +2241,7 @@ public class AppOpsManager { AppOpsManager.MODE_DEFAULT, // INTERACT_ACROSS_PROFILES AppOpsManager.MODE_IGNORED, // ACTIVATE_PLATFORM_VPN AppOpsManager.MODE_DEFAULT, // LOADER_USAGE_STATS - AppOpsManager.MODE_DEFAULT, // ACCESS_CALL_AUDIO + AppOpsManager.MODE_IGNORED, // deprecated operation AppOpsManager.MODE_DEFAULT, // OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED AppOpsManager.MODE_ALLOWED, // OP_AUTO_REVOKE_MANAGED_BY_INSTALLER }; @@ -2353,7 +2350,7 @@ public class AppOpsManager { false, // INTERACT_ACROSS_PROFILES false, // ACTIVATE_PLATFORM_VPN false, // LOADER_USAGE_STATS - false, // ACCESS_CALL_AUDIO + false, // deprecated operation false, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED false, // AUTO_REVOKE_MANAGED_BY_INSTALLER }; diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 50bee854c027..1e0b2e358e17 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1118,6 +1118,7 @@ public class PackageInstaller { * {@hide} */ @SystemApi + @RequiresPermission(android.Manifest.permission.USE_INSTALLER_V2) public @Nullable DataLoaderParams getDataLoaderParams() { try { DataLoaderParamsParcel data = mSession.getDataLoaderParams(); @@ -1157,6 +1158,7 @@ public class PackageInstaller { * {@hide} */ @SystemApi + @RequiresPermission(android.Manifest.permission.USE_INSTALLER_V2) public void addFile(@FileLocation int location, @NonNull String name, long lengthBytes, @NonNull byte[] metadata, @Nullable byte[] signature) { try { @@ -1180,6 +1182,7 @@ public class PackageInstaller { * {@hide} */ @SystemApi + @RequiresPermission(android.Manifest.permission.USE_INSTALLER_V2) public void removeFile(@FileLocation int location, @NonNull String name) { try { mSession.removeFile(location, name); @@ -1927,7 +1930,9 @@ public class PackageInstaller { * {@hide} */ @SystemApi - @RequiresPermission(Manifest.permission.INSTALL_PACKAGES) + @RequiresPermission(allOf = { + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.USE_INSTALLER_V2}) public void setDataLoaderParams(@NonNull DataLoaderParams dataLoaderParams) { this.dataLoaderParams = dataLoaderParams; } diff --git a/core/proto/android/app/enums.proto b/core/proto/android/app/enums.proto index 42437d5b44a0..563ef145b79c 100644 --- a/core/proto/android/app/enums.proto +++ b/core/proto/android/app/enums.proto @@ -203,9 +203,7 @@ enum AppOpEnum { APP_OP_INTERACT_ACROSS_PROFILES = 93; APP_OP_ACTIVATE_PLATFORM_VPN = 94; APP_OP_LOADER_USAGE_STATS = 95; - APP_OP_ACCESS_CALL_AUDIO = 96; + APP_OP_DEPRECATED_1 = 96 [deprecated = true]; APP_OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED = 97; APP_OP_AUTO_REVOKE_MANAGED_BY_INSTALLER = 98; } - - diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 84d9ce288069..eae614546dfb 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1213,15 +1213,6 @@ android:description="@string/permdesc_acceptHandovers" android:protectionLevel="dangerous" /> - <!-- Allows an application assigned to the Dialer role to be granted access to the telephony - call audio streams, both TX and RX. - <p>Protection level: signature|appop - --> - <permission android:name="android.permission.ACCESS_CALL_AUDIO" - android.label="@string/permlab_accessCallAudio" - android:description="@string/permdesc_accessCallAudio" - android:protectionLevel="signature|appop" /> - <!-- ====================================================================== --> <!-- Permissions for accessing the device microphone --> <!-- ====================================================================== --> @@ -3654,6 +3645,16 @@ <permission android:name="com.android.permission.INSTALL_EXISTING_PACKAGES" android:protectionLevel="signature|privileged" /> + <!-- Allows an application to use the package installer v2 APIs. + <p>The package installer v2 APIs are still a work in progress and we're + currently validating they work in all scenarios. + <p>Not for use by third-party applications. + TODO(b/152310230): remove this permission once the APIs are confirmed to be sufficient. + @hide + --> + <permission android:name="com.android.permission.USE_INSTALLER_V2" + android:protectionLevel="signature|verifier" /> + <!-- @SystemApi @TestApi Allows an application to clear user data. <p>Not for use by third-party applications @hide diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 06f776013233..5b880365657c 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5482,11 +5482,6 @@ <!-- Error message. This text lets the user know that their current personal apps can't open this specific content. [CHAR LIMIT=NONE] --> <string name="resolver_no_personal_apps_available_resolve">No personal apps can open this content</string> - <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE] --> - <string name="permlab_accessCallAudio">Record or play audio in telephony calls</string> - <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE] --> - <string name="permdesc_accessCallAudio">Allows this app, when assigned as default dialer application, to record or play audio in telephony calls.</string> - <!-- Icc depersonalization related strings --> <!-- Label text for PIN entry widget on SIM Network Depersonalization panel [CHAR LIMIT=none] --> <string name="PERSOSUBSTATE_SIM_NETWORK_ENTRY">SIM network unlock PIN</string> diff --git a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java index 1b5ce8fd4ce5..9a93dbf67d33 100644 --- a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java +++ b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.assertTrue; import android.app.Activity; import android.app.Instrumentation; import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; @@ -48,7 +49,7 @@ import android.view.MotionEvent; import android.view.View; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; +import androidx.test.filters.MediumTest; import androidx.test.filters.Suppress; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; @@ -67,7 +68,8 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @RunWith(AndroidJUnit4.class) -@SmallTest +@MediumTest +@Presubmit public class EditorCursorDragTest { private static final String LOG_TAG = EditorCursorDragTest.class.getSimpleName(); diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java index 3dc001d68a02..ec75e40f1334 100644 --- a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java +++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java @@ -22,6 +22,7 @@ import static junit.framework.Assert.assertTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import android.platform.test.annotations.Presubmit; import android.view.InputDevice; import android.view.MotionEvent; import android.view.ViewConfiguration; @@ -36,6 +37,7 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) @SmallTest +@Presubmit public class EditorTouchStateTest { private EditorTouchState mTouchState; diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java index 8bf48e59165e..c713d7813a54 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java @@ -17,6 +17,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; @@ -36,7 +37,10 @@ import com.android.settingslib.R; import java.util.List; -public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { +/** + * Track status of Wi-Fi for the Sys UI. + */ +public class WifiStatusTracker { private final Context mContext; private final WifiNetworkScoreCache mWifiNetworkScoreCache; private final WifiManager mWifiManager; @@ -55,8 +59,9 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { .clearCapabilities() .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); - private final ConnectivityManager.NetworkCallback mNetworkCallback = new ConnectivityManager - .NetworkCallback() { + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + // Note: onCapabilitiesChanged is guaranteed to be called "immediately" after onAvailable + // and onLinkPropertiesChanged. @Override public void onCapabilitiesChanged( Network network, NetworkCapabilities networkCapabilities) { @@ -64,11 +69,35 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { mCallback.run(); } }; + private final NetworkCallback mDefaultNetworkCallback = new NetworkCallback() { + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + // network is now the default network, and its capabilities are nc. + // This method will always be called immediately after the network becomes the + // default, in addition to any time the capabilities change while the network is + // the default. + mDefaultNetwork = network; + mDefaultNetworkCapabilities = nc; + updateStatusLabel(); + mCallback.run(); + } + @Override + public void onLost(Network network) { + // The system no longer has a default network. + mDefaultNetwork = null; + mDefaultNetworkCapabilities = null; + updateStatusLabel(); + mCallback.run(); + } + }; + private Network mDefaultNetwork = null; + private NetworkCapabilities mDefaultNetworkCapabilities = null; private final Runnable mCallback; private WifiInfo mWifiInfo; public boolean enabled; public boolean isCaptivePortal; + public boolean isDefaultNetwork; public int state; public boolean connected; public String ssid; @@ -94,11 +123,13 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { mWifiNetworkScoreCache.registerListener(mCacheListener); mConnectivityManager.registerNetworkCallback( mNetworkRequest, mNetworkCallback, mHandler); + mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, mHandler); } else { mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mWifiNetworkScoreCache); mWifiNetworkScoreCache.unregisterListener(); mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback); } } @@ -154,8 +185,17 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { } private void updateStatusLabel() { - final NetworkCapabilities networkCapabilities - = mConnectivityManager.getNetworkCapabilities(mWifiManager.getCurrentNetwork()); + NetworkCapabilities networkCapabilities; + final Network currentWifiNetwork = mWifiManager.getCurrentNetwork(); + if (currentWifiNetwork != null && currentWifiNetwork.equals(mDefaultNetwork)) { + // Wifi is connected and the default network. + isDefaultNetwork = true; + networkCapabilities = mDefaultNetworkCapabilities; + } else { + isDefaultNetwork = false; + networkCapabilities = mConnectivityManager.getNetworkCapabilities( + mWifiManager.getCurrentNetwork()); + } isCaptivePortal = false; if (networkCapabilities != null) { if (networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index c7fb00a8130c..4771c4139a5b 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -81,6 +81,8 @@ <uses-permission android:name="android.permission.READ_INPUT_STATE" /> <uses-permission android:name="android.permission.SET_ORIENTATION" /> <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> + <!-- TODO(b/152310230): remove once APIs are confirmed to be sufficient --> + <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" /> <uses-permission android:name="android.permission.MOVE_PACKAGE" /> <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" /> <uses-permission android:name="android.permission.CLEAR_APP_CACHE" /> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c8c35c704297..8a3a16e9a6cf 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -374,9 +374,31 @@ <string name="biometric_dialog_wrong_password">Wrong password</string> <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]--> <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string> + <!-- Error string shown when the user enters an incorrect PIN/pattern/password and it counts towards the max attempts before the data on the device is wiped. [CHAR LIMIT=NONE]--> <string name="biometric_dialog_credential_attempts_before_wipe">Try again. Attempt <xliff:g id="attempts" example="1">%1$d</xliff:g> of <xliff:g id="max_attempts" example="3">%2$d</xliff:g>.</string> + <!-- Title of a dialog shown when the user only has one attempt left to provide the correct PIN/pattern/password before the device, one of its users, or a work profile is wiped. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_attempt_before_wipe_dialog_title">Your data will be deleted</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct lock pattern before the device is wiped. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pattern_attempt_before_wipe_device">If you enter an incorrect pattern on the next attempt, this device\u2019s data will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct PIN before the device is wiped. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pin_attempt_before_wipe_device">If you enter an incorrect PIN on the next attempt, this device\u2019s data will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct password before the device is wiped. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_password_attempt_before_wipe_device">If you enter an incorrect password on the next attempt, this device\u2019s data will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct lock pattern before the user is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pattern_attempt_before_wipe_user">If you enter an incorrect pattern on the next attempt, this user will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct PIN before the user is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pin_attempt_before_wipe_user">If you enter an incorrect PIN on the next attempt, this user will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct password before the user is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_password_attempt_before_wipe_user">If you enter an incorrect password on the next attempt, this user will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct pattern before the work profile is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pattern_attempt_before_wipe_profile">If you enter an incorrect pattern on the next attempt, your work profile and its data will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct PIN before the work profile is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_pin_attempt_before_wipe_profile">If you enter an incorrect PIN on the next attempt, your work profile and its data will be deleted.</string> + <!-- Content of a dialog shown when the user only has one attempt left to provide the correct password before the work profile is removed. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_last_password_attempt_before_wipe_profile">If you enter an incorrect password on the next attempt, your work profile and its data will be deleted.</string> + <!-- Content of a dialog shown when the user has failed to provide the device lock too many times and the device is wiped. [CHAR LIMIT=NONE] --> <string name="biometric_dialog_failed_attempts_now_wiping_device">Too many incorrect attempts. This device\u2019s data will be deleted.</string> <!-- Content of a dialog shown when the user has failed to provide the user lock too many times and the user is removed. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 13f3c0fce5c2..b006bc1351a3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -347,21 +347,35 @@ public abstract class AuthCredentialView extends LinearLayout { showError(message); } - // Only show popup dialog before wipe. + // Only show dialog if <=1 attempts are left before wiping. final int remainingAttempts = maxAttempts - numAttempts; - if (remainingAttempts <= 0) { - showNowWipingMessage(); - mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR); + if (remainingAttempts == 1) { + showLastAttemptBeforeWipeDialog(); + } else if (remainingAttempts <= 0) { + showNowWipingDialog(); } return true; } - private void showNowWipingMessage() { + private void showLastAttemptBeforeWipeDialog() { + final AlertDialog alertDialog = new AlertDialog.Builder(mContext) + .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) + .setMessage( + getLastAttemptBeforeWipeMessageRes(getUserTypeForWipe(), mCredentialType)) + .setPositiveButton(android.R.string.ok, null) + .create(); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.show(); + } + + private void showNowWipingDialog() { final AlertDialog alertDialog = new AlertDialog.Builder(mContext) .setMessage(getNowWipingMessageRes(getUserTypeForWipe())) .setPositiveButton(R.string.biometric_dialog_now_wiping_dialog_dismiss, null) + .setOnDismissListener( + dialog -> mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR)) .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); alertDialog.show(); } @@ -377,6 +391,59 @@ public abstract class AuthCredentialView extends LinearLayout { } } + private static @StringRes int getLastAttemptBeforeWipeMessageRes( + @UserType int userType, @Utils.CredentialType int credentialType) { + switch (userType) { + case USER_TYPE_PRIMARY: + return getLastAttemptBeforeWipeDeviceMessageRes(credentialType); + case USER_TYPE_MANAGED_PROFILE: + return getLastAttemptBeforeWipeProfileMessageRes(credentialType); + case USER_TYPE_SECONDARY: + return getLastAttemptBeforeWipeUserMessageRes(credentialType); + default: + throw new IllegalArgumentException("Unrecognized user type:" + userType); + } + } + + private static @StringRes int getLastAttemptBeforeWipeDeviceMessageRes( + @Utils.CredentialType int credentialType) { + switch (credentialType) { + case Utils.CREDENTIAL_PIN: + return R.string.biometric_dialog_last_pin_attempt_before_wipe_device; + case Utils.CREDENTIAL_PATTERN: + return R.string.biometric_dialog_last_pattern_attempt_before_wipe_device; + case Utils.CREDENTIAL_PASSWORD: + default: + return R.string.biometric_dialog_last_password_attempt_before_wipe_device; + } + } + + private static @StringRes int getLastAttemptBeforeWipeProfileMessageRes( + @Utils.CredentialType int credentialType) { + switch (credentialType) { + case Utils.CREDENTIAL_PIN: + return R.string.biometric_dialog_last_pin_attempt_before_wipe_profile; + case Utils.CREDENTIAL_PATTERN: + return R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile; + case Utils.CREDENTIAL_PASSWORD: + default: + return R.string.biometric_dialog_last_password_attempt_before_wipe_profile; + } + } + + private static @StringRes int getLastAttemptBeforeWipeUserMessageRes( + @Utils.CredentialType int credentialType) { + switch (credentialType) { + case Utils.CREDENTIAL_PIN: + return R.string.biometric_dialog_last_pin_attempt_before_wipe_user; + case Utils.CREDENTIAL_PATTERN: + return R.string.biometric_dialog_last_pattern_attempt_before_wipe_user; + case Utils.CREDENTIAL_PASSWORD: + default: + return R.string.biometric_dialog_last_password_attempt_before_wipe_user; + } + } + private static @StringRes int getNowWipingMessageRes(@UserType int userType) { switch (userType) { case USER_TYPE_PRIMARY: diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 6c49c82acdc0..118fcbb20f26 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -252,10 +252,17 @@ class ControlsControllerImpl @Inject constructor ( it.controlId in favoritesForComponentKeys ) } + val removedControls = mutableListOf<ControlStatus>() + Favorites.getStructuresForComponent(componentName).forEach { st -> + st.controls.forEach { + if (it.controlId in removed) { + val r = createRemovedStatus(componentName, it, st.structure) + removedControls.add(r) + } + } + } val loadData = createLoadDataObject( - Favorites.getControlsForComponent(componentName) - .filter { it.controlId in removed } - .map { createRemovedStatus(componentName, it) } + + removedControls + controlsWithFavorite, favoritesForComponentKeys ) @@ -266,17 +273,15 @@ class ControlsControllerImpl @Inject constructor ( override fun error(message: String) { loadCanceller = null executor.execute { - val loadData = Favorites.getControlsForComponent(componentName) - .let { controls -> - val keys = controls.map { it.controlId } - createLoadDataObject( - controls.map { - createRemovedStatus(componentName, it, false) - }, - keys, - true - ) - } + val controls = Favorites.getStructuresForComponent(componentName) + .flatMap { st -> + st.controls.map { + createRemovedStatus(componentName, it, st.structure, + false) + } + } + val keys = controls.map { it.control.controlId } + val loadData = createLoadDataObject(controls, keys, true) dataCallback.accept(loadData) } } @@ -372,6 +377,7 @@ class ControlsControllerImpl @Inject constructor ( private fun createRemovedStatus( componentName: ComponentName, controlInfo: ControlInfo, + structure: CharSequence, setRemoved: Boolean = true ): ControlStatus { val intent = Intent(Intent.ACTION_MAIN).apply { @@ -384,6 +390,8 @@ class ControlsControllerImpl @Inject constructor ( 0) val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent) .setTitle(controlInfo.controlTitle) + .setSubtitle(controlInfo.controlSubtitle) + .setStructure(structure) .setDeviceType(controlInfo.deviceType) .build() return ControlStatus(control, componentName, true, setRemoved) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java index 7c963869ed47..c2fc18fe21a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -37,10 +37,10 @@ import java.util.Objects; public class WifiSignalController extends SignalController<WifiSignalController.WifiState, SignalController.IconGroup> { - private final boolean mHasMobileData; + private final boolean mHasMobileDataFeature; private final WifiStatusTracker mWifiTracker; - public WifiSignalController(Context context, boolean hasMobileData, + public WifiSignalController(Context context, boolean hasMobileDataFeature, CallbackHandler callbackHandler, NetworkControllerImpl networkController, WifiManager wifiManager) { super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, @@ -52,7 +52,7 @@ public class WifiSignalController extends mWifiTracker = new WifiStatusTracker(mContext, wifiManager, networkScoreManager, connectivityManager, this::handleStatusUpdated); mWifiTracker.setListening(true); - mHasMobileData = hasMobileData; + mHasMobileDataFeature = hasMobileDataFeature; if (wifiManager != null) { wifiManager.registerTrafficStateCallback(context.getMainExecutor(), new WifiTrafficStateCallback()); @@ -85,9 +85,10 @@ public class WifiSignalController extends // only show wifi in the cluster if connected or if wifi-only boolean visibleWhenEnabled = mContext.getResources().getBoolean( R.bool.config_showWifiIndicatorWhenEnabled); - boolean wifiVisible = mCurrentState.enabled - && ((mCurrentState.connected && mCurrentState.inetCondition == 1) - || !mHasMobileData || visibleWhenEnabled); + boolean wifiVisible = mCurrentState.enabled && ( + (mCurrentState.connected && mCurrentState.inetCondition == 1) + || !mHasMobileDataFeature || mWifiTracker.isDefaultNetwork + || visibleWhenEnabled); String wifiDesc = mCurrentState.connected ? mCurrentState.ssid : null; boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; String contentDescription = getTextIfExists(getContentDescription()).toString(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index d5a654dc2b6f..eb4d438600d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -307,6 +307,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, controls.size) val controlStatus = controls[0] assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId) + assertEquals(TEST_STRUCTURE_INFO.structure, controlStatus.control.structure) assertTrue(controlStatus.favorite) assertTrue(controlStatus.removed) @@ -337,6 +338,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, controls.size) val controlStatus = controls[0] assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId) + assertEquals(TEST_STRUCTURE_INFO.structure, controlStatus.control.structure) assertTrue(controlStatus.favorite) assertFalse(controlStatus.removed) diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 2ff3d2a3a938..8ff7ea9d61dd 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -490,6 +490,14 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements throw new SecurityException("User restriction prevents installing"); } + if (params.dataLoaderParams != null + && mContext.checkCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("You need the " + + "com.android.permission.USE_INSTALLER_V2 permission " + + "to use a data loader"); + } + String requestedInstallerPackageName = params.installerPackageName != null ? params.installerPackageName : installerPackageName; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 348a4cb77163..42ed20654a2c 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -2479,12 +2479,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public DataLoaderParamsParcel getDataLoaderParams() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); return params.dataLoaderParams != null ? params.dataLoaderParams.getData() : null; } @Override public void addFile(int location, String name, long lengthBytes, byte[] metadata, byte[] signature) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); if (!isDataLoaderInstallation()) { throw new IllegalStateException( "Cannot add files to non-data loader installation session."); @@ -2517,6 +2519,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Override public void removeFile(int location, String name) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); if (!isDataLoaderInstallation()) { throw new IllegalStateException( "Cannot add files to non-data loader installation session."); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java index b051bab71f12..8ff2a1b6364b 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java @@ -37,9 +37,9 @@ class TimeZoneDetectorShellCommand extends ShellCommand { } switch (cmd) { - case "suggestTelephonyTimeZone": + case "suggest_telephony_time_zone": return runSuggestTelephonyTimeZone(); - case "suggestManualTimeZone": + case "suggest_manual_time_zone": return runSuggestManualTimeZone(); default: { return handleDefaultCommands(cmd); @@ -105,9 +105,9 @@ class TimeZoneDetectorShellCommand extends ShellCommand { pw.println("Time Zone Detector (time_zone_detector) commands:"); pw.println(" help"); pw.println(" Print this help text."); - pw.println(" suggestTelephonyTimeZone"); + pw.println(" suggest_telephony_time_zone"); pw.println(" --suggestion <telephony suggestion opts>"); - pw.println(" suggestManualTimeZone"); + pw.println(" suggest_manual_time_zone"); pw.println(" --suggestion <manual suggestion opts>"); pw.println(); ManualTimeZoneSuggestion.printCommandLineOpts(pw); diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index ae8d5743668a..136ee91dd685 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -26,6 +26,7 @@ import android.app.NotificationManager; import android.app.Person; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; +import android.app.usage.UsageEvents; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -70,6 +71,7 @@ import com.android.server.notification.NotificationManagerInternal; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -237,6 +239,27 @@ public class DataManager { eventHistory.addEvent(new Event(System.currentTimeMillis(), eventType)); } + /** + * Queries events for moving app to foreground between {@code startTime} and {@code endTime}. + */ + @NonNull + public List<UsageEvents.Event> queryAppMovingToForegroundEvents(@UserIdInt int callingUserId, + long startTime, long endTime) { + return UsageStatsQueryHelper.queryAppMovingToForegroundEvents(callingUserId, startTime, + endTime); + } + + /** + * Queries launch counts of apps within {@code packageNameFilter} between {@code startTime} + * and {@code endTime}. + */ + @NonNull + public Map<String, Integer> queryAppLaunchCount(@UserIdInt int callingUserId, long startTime, + long endTime, Set<String> packageNameFilter) { + return UsageStatsQueryHelper.queryAppLaunchCount(callingUserId, startTime, endTime, + packageNameFilter); + } + /** Prunes the data for the specified user. */ public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) { UserData userData = getUnlockedUserData(userId); @@ -382,7 +405,13 @@ public class DataManager { } } - private int mimeTypeToShareEventType(String mimeType) { + /** + * Converts {@code mimeType} to {@link Event.EventType}. + */ + public int mimeTypeToShareEventType(String mimeType) { + if (mimeType == null) { + return Event.TYPE_SHARE_OTHER; + } if (mimeType.startsWith("text/")) { return Event.TYPE_SHARE_TEXT; } else if (mimeType.startsWith("image/")) { diff --git a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java index 72f1abb70e34..6e6fea93c803 100644 --- a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java @@ -19,6 +19,8 @@ package com.android.server.people.data; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.LocusId; @@ -27,7 +29,10 @@ import android.util.ArrayMap; import com.android.server.LocalServices; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; /** A helper class that queries {@link UsageStatsManagerInternal}. */ @@ -46,7 +51,7 @@ class UsageStatsQueryHelper { */ UsageStatsQueryHelper(@UserIdInt int userId, Function<String, PackageData> packageDataGetter) { - mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class); + mUsageStatsManagerInternal = getUsageStatsManagerInternal(); mUserId = userId; mPackageDataGetter = packageDataGetter; } @@ -106,6 +111,53 @@ class UsageStatsQueryHelper { return mLastEventTimestamp; } + /** + * Queries {@link UsageStatsManagerInternal} events for moving app to foreground between + * {@code startTime} and {@code endTime}. + * + * @return a list containing events moving app to foreground. + */ + static List<UsageEvents.Event> queryAppMovingToForegroundEvents(@UserIdInt int userId, + long startTime, long endTime) { + List<UsageEvents.Event> res = new ArrayList<>(); + UsageEvents usageEvents = getUsageStatsManagerInternal().queryEventsForUser(userId, + startTime, endTime, + UsageEvents.HIDE_SHORTCUT_EVENTS | UsageEvents.HIDE_LOCUS_EVENTS); + if (usageEvents == null) { + return res; + } + while (usageEvents.hasNextEvent()) { + UsageEvents.Event e = new UsageEvents.Event(); + usageEvents.getNextEvent(e); + if (e.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED) { + res.add(e); + } + } + return res; + } + + /** + * Queries {@link UsageStatsManagerInternal} for launch count of apps within {@code + * packageNameFilter} between {@code startTime} and {@code endTime}.obfuscateInstantApps + * + * @return a map which keys are package names and values are app launch counts. + */ + static Map<String, Integer> queryAppLaunchCount(@UserIdInt int userId, long startTime, + long endTime, Set<String> packageNameFilter) { + List<UsageStats> stats = getUsageStatsManagerInternal().queryUsageStatsForUser(userId, + UsageStatsManager.INTERVAL_BEST, startTime, endTime, + /* obfuscateInstantApps= */ false); + Map<String, Integer> aggregatedStats = new ArrayMap<>(); + for (UsageStats stat : stats) { + String packageName = stat.getPackageName(); + if (packageNameFilter.contains(packageName)) { + aggregatedStats.put(packageName, + aggregatedStats.getOrDefault(packageName, 0) + stat.getAppLaunchCount()); + } + } + return aggregatedStats; + } + private void onInAppConversationEnded(@NonNull PackageData packageData, @NonNull UsageEvents.Event endEvent) { ComponentName activityName = @@ -138,4 +190,8 @@ class UsageStatsQueryHelper { EventStore.CATEGORY_LOCUS_ID_BASED, locusId.getId()); eventHistory.addEvent(event); } + + private static UsageStatsManagerInternal getUsageStatsManagerInternal() { + return LocalServices.getService(UsageStatsManagerInternal.class); + } } diff --git a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java index 8e5d75be12b7..d09d0b379769 100644 --- a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java +++ b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java @@ -27,13 +27,11 @@ import android.app.prediction.AppTargetId; import android.content.IntentFilter; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager.ShareShortcutInfo; -import android.util.Range; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ChooserActivity; import com.android.server.people.data.ConversationInfo; import com.android.server.people.data.DataManager; -import com.android.server.people.data.Event; import com.android.server.people.data.EventHistory; import com.android.server.people.data.PackageData; @@ -42,6 +40,9 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; +/** + * Predictor that predicts the {@link AppTarget} the user is most likely to open on share sheet. + */ class ShareTargetPredictor extends AppTargetPredictor { private final IntentFilter mIntentFilter; @@ -66,7 +67,9 @@ class ShareTargetPredictor extends AppTargetPredictor { @Override void predictTargets() { List<ShareTarget> shareTargets = getDirectShareTargets(); - rankTargets(shareTargets); + SharesheetModelScorer.computeScore(shareTargets, getShareEventType(mIntentFilter), + System.currentTimeMillis()); + Collections.sort(shareTargets, (t1, t2) -> -Float.compare(t1.getScore(), t2.getScore())); List<AppTarget> res = new ArrayList<>(); for (int i = 0; i < Math.min(getPredictionContext().getPredictedTargetCount(), shareTargets.size()); i++) { @@ -80,36 +83,16 @@ class ShareTargetPredictor extends AppTargetPredictor { @Override void sortTargets(List<AppTarget> targets, Consumer<List<AppTarget>> callback) { List<ShareTarget> shareTargets = getAppShareTargets(targets); - rankTargets(shareTargets); + SharesheetModelScorer.computeScoreForAppShare(shareTargets, + getShareEventType(mIntentFilter), getPredictionContext().getPredictedTargetCount(), + System.currentTimeMillis(), getDataManager(), + mCallingUserId); + Collections.sort(shareTargets, (t1, t2) -> -Float.compare(t1.getScore(), t2.getScore())); List<AppTarget> appTargetList = new ArrayList<>(); shareTargets.forEach(t -> appTargetList.add(t.getAppTarget())); callback.accept(appTargetList); } - private void rankTargets(List<ShareTarget> shareTargets) { - // Rank targets based on recency of sharing history only for the moment. - // TODO: Take more factors into ranking, e.g. frequency, mime type, foreground app. - Collections.sort(shareTargets, (t1, t2) -> { - if (t1.getEventHistory() == null) { - return 1; - } - if (t2.getEventHistory() == null) { - return -1; - } - Range<Long> timeSlot1 = t1.getEventHistory().getEventIndex( - Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); - Range<Long> timeSlot2 = t2.getEventHistory().getEventIndex( - Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); - if (timeSlot1 == null) { - return 1; - } else if (timeSlot2 == null) { - return -1; - } else { - return -Long.compare(timeSlot1.getUpper(), timeSlot2.getUpper()); - } - }); - } - private List<ShareTarget> getDirectShareTargets() { List<ShareTarget> shareTargets = new ArrayList<>(); List<ShareShortcutInfo> shareShortcuts = @@ -153,6 +136,11 @@ class ShareTargetPredictor extends AppTargetPredictor { return shareTargets; } + private int getShareEventType(IntentFilter intentFilter) { + String mimeType = intentFilter != null ? intentFilter.getDataType(0) : null; + return getDataManager().mimeTypeToShareEventType(mimeType); + } + @VisibleForTesting static class ShareTarget { @@ -162,13 +150,16 @@ class ShareTargetPredictor extends AppTargetPredictor { private final EventHistory mEventHistory; @Nullable private final ConversationInfo mConversationInfo; + private float mScore; - private ShareTarget(@NonNull AppTarget appTarget, + @VisibleForTesting + ShareTarget(@NonNull AppTarget appTarget, @Nullable EventHistory eventHistory, @Nullable ConversationInfo conversationInfo) { mAppTarget = appTarget; mEventHistory = eventHistory; mConversationInfo = conversationInfo; + mScore = 0f; } @NonNull @@ -188,5 +179,15 @@ class ShareTargetPredictor extends AppTargetPredictor { ConversationInfo getConversationInfo() { return mConversationInfo; } + + @VisibleForTesting + float getScore() { + return mScore; + } + + @VisibleForTesting + void setScore(float score) { + mScore = score; + } } } diff --git a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java new file mode 100644 index 000000000000..0ac5724210da --- /dev/null +++ b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 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.people.prediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.usage.UsageEvents; +import android.util.ArrayMap; +import android.util.Pair; +import android.util.Range; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ChooserActivity; +import com.android.server.people.data.DataManager; +import com.android.server.people.data.Event; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.concurrent.TimeUnit; + +/** Ranking scorer for Sharesheet targets. */ +class SharesheetModelScorer { + + private static final String TAG = "SharesheetModelScorer"; + private static final boolean DEBUG = false; + private static final Integer RECENCY_SCORE_COUNT = 6; + private static final float RECENCY_INITIAL_BASE_SCORE = 0.4F; + private static final float RECENCY_SCORE_INITIAL_DECAY = 0.05F; + private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F; + private static final long ONE_MONTH_WINDOW = TimeUnit.DAYS.toMillis(30); + private static final long FOREGROUND_APP_PROMO_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10); + private static final float FREQUENTLY_USED_APP_SCORE_DECAY = 0.9F; + @VisibleForTesting + static final float FOREGROUND_APP_WEIGHT = 0F; + @VisibleForTesting + static final String CHOOSER_ACTIVITY = ChooserActivity.class.getSimpleName(); + + // Keep constructor private to avoid class being instantiated. + private SharesheetModelScorer() { + } + + /** + * Computes each target's recency, frequency and frequency of the same {@code shareEventType} + * based on past sharing history. Update {@link ShareTargetPredictor.ShareTargetScore}. + */ + static void computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets, + int shareEventType, long now) { + if (shareTargets.isEmpty()) { + return; + } + float totalFreqScore = 0f; + int freqScoreCount = 0; + float totalMimeFreqScore = 0f; + int mimeFreqScoreCount = 0; + // Top of this heap has lowest rank. + PriorityQueue<Pair<ShareTargetRankingScore, Range<Long>>> recencyMinHeap = + new PriorityQueue<>(RECENCY_SCORE_COUNT, + Comparator.comparingLong(p -> p.second.getUpper())); + List<ShareTargetRankingScore> scoreList = new ArrayList<>(shareTargets.size()); + for (ShareTargetPredictor.ShareTarget target : shareTargets) { + ShareTargetRankingScore shareTargetScore = new ShareTargetRankingScore(); + scoreList.add(shareTargetScore); + if (target.getEventHistory() == null) { + continue; + } + // Counts frequency + List<Range<Long>> timeSlots = target.getEventHistory().getEventIndex( + Event.SHARE_EVENT_TYPES).getActiveTimeSlots(); + if (!timeSlots.isEmpty()) { + for (Range<Long> timeSlot : timeSlots) { + shareTargetScore.incrementFrequencyScore( + getFreqDecayedOnElapsedTime(now - timeSlot.getLower())); + } + totalFreqScore += shareTargetScore.getFrequencyScore(); + freqScoreCount++; + } + // Counts frequency for sharing same mime type + List<Range<Long>> timeSlotsOfSameType = target.getEventHistory().getEventIndex( + shareEventType).getActiveTimeSlots(); + if (!timeSlotsOfSameType.isEmpty()) { + for (Range<Long> timeSlot : timeSlotsOfSameType) { + shareTargetScore.incrementMimeFrequencyScore( + getFreqDecayedOnElapsedTime(now - timeSlot.getLower())); + } + totalMimeFreqScore += shareTargetScore.getMimeFrequencyScore(); + mimeFreqScoreCount++; + } + // Records most recent targets + Range<Long> mostRecentTimeSlot = target.getEventHistory().getEventIndex( + Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); + if (mostRecentTimeSlot == null) { + continue; + } + if (recencyMinHeap.size() < RECENCY_SCORE_COUNT + || mostRecentTimeSlot.getUpper() > recencyMinHeap.peek().second.getUpper()) { + if (recencyMinHeap.size() == RECENCY_SCORE_COUNT) { + recencyMinHeap.poll(); + } + recencyMinHeap.offer(new Pair(shareTargetScore, mostRecentTimeSlot)); + } + } + // Calculates recency score + while (!recencyMinHeap.isEmpty()) { + float recencyScore = RECENCY_INITIAL_BASE_SCORE; + if (recencyMinHeap.size() > 1) { + recencyScore = RECENCY_INITIAL_BASE_SCORE - RECENCY_SCORE_INITIAL_DECAY + - RECENCY_SCORE_SUBSEQUENT_DECAY * (recencyMinHeap.size() - 2); + } + recencyMinHeap.poll().first.setRecencyScore(recencyScore); + } + + Float avgFreq = freqScoreCount != 0 ? totalFreqScore / freqScoreCount : 0f; + Float avgMimeFreq = mimeFreqScoreCount != 0 ? totalMimeFreqScore / mimeFreqScoreCount : 0f; + for (int i = 0; i < scoreList.size(); i++) { + ShareTargetPredictor.ShareTarget target = shareTargets.get(i); + ShareTargetRankingScore targetScore = scoreList.get(i); + // Normalizes freq and mimeFreq score + targetScore.setFrequencyScore(normalizeFreqScore( + avgFreq.equals(0f) ? 0f : targetScore.getFrequencyScore() / avgFreq)); + targetScore.setMimeFrequencyScore(normalizeMimeFreqScore(avgMimeFreq.equals(0f) ? 0f + : targetScore.getMimeFrequencyScore() / avgMimeFreq)); + // Calculates total score + targetScore.setTotalScore( + probOR(probOR(targetScore.getRecencyScore(), targetScore.getFrequencyScore()), + targetScore.getMimeFrequencyScore())); + target.setScore(targetScore.getTotalScore()); + + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: packageName: %s, className: %s, shortcutId: %s, " + + "recency:%.2f, freq_all:%.2f, freq_mime:%.2f, total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getAppTarget().getShortcutInfo() != null + ? target.getAppTarget().getShortcutInfo().getId() : null, + targetScore.getRecencyScore(), + targetScore.getFrequencyScore(), + targetScore.getMimeFrequencyScore(), + targetScore.getTotalScore())); + } + } + } + + /** + * Computes ranking score for app sharing. Update {@link ShareTargetPredictor.ShareTargetScore}. + */ + static void computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets, + int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager, + @UserIdInt int callingUserId) { + computeScore(shareTargets, shareEventType, now); + postProcess(shareTargets, targetsLimit, dataManager, callingUserId); + } + + private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets, + int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + // Populates a map which key is package name and value is list of shareTargets descended + // on total score. + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap = new ArrayMap<>(); + for (ShareTargetPredictor.ShareTarget shareTarget : shareTargets) { + String packageName = shareTarget.getAppTarget().getPackageName(); + shareTargetMap.computeIfAbsent(packageName, key -> new ArrayList<>()); + List<ShareTargetPredictor.ShareTarget> targetsList = shareTargetMap.get(packageName); + int index = 0; + while (index < targetsList.size()) { + if (shareTarget.getScore() > targetsList.get(index).getScore()) { + break; + } + index++; + } + targetsList.add(index, shareTarget); + } + promoteForegroundApp(shareTargetMap, dataManager, callingUserId); + promoteFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, callingUserId); + } + + /** + * Promotes frequently used sharing apps, if recommended apps based on sharing history have not + * reached the limit (e.g. user did not share any content in last couple weeks) + */ + private static void promoteFrequentlyUsedApps( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + int validPredictionNum = 0; + float minValidScore = 1f; + for (List<ShareTargetPredictor.ShareTarget> targets : shareTargetMap.values()) { + for (ShareTargetPredictor.ShareTarget target : targets) { + if (target.getScore() > 0f) { + validPredictionNum++; + minValidScore = Math.min(target.getScore(), minValidScore); + } + } + } + // Skips if recommended apps based on sharing history have already reached the limit. + if (validPredictionNum >= targetsLimit) { + return; + } + long now = System.currentTimeMillis(); + Map<String, Integer> appLaunchCountsMap = dataManager.queryAppLaunchCount( + callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet()); + List<Pair<String, Integer>> appLaunchCounts = new ArrayList<>(); + for (Map.Entry<String, Integer> entry : appLaunchCountsMap.entrySet()) { + if (entry.getValue() > 0) { + appLaunchCounts.add(new Pair(entry.getKey(), entry.getValue())); + } + } + Collections.sort(appLaunchCounts, (p1, p2) -> -Integer.compare(p1.second, p2.second)); + for (Pair<String, Integer> entry : appLaunchCounts) { + if (!shareTargetMap.containsKey(entry.first)) { + continue; + } + ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.first).get(0); + if (target.getScore() > 0f) { + continue; + } + minValidScore *= FREQUENTLY_USED_APP_SCORE_DECAY; + target.setScore(minValidScore); + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: promoteFrequentUsedApps packageName: %s, className: %s," + + " total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getScore())); + } + validPredictionNum++; + if (validPredictionNum == targetsLimit) { + return; + } + } + } + + /** + * Promotes the foreground app just prior to source sharing app. Share often occurs between + * two apps the user is switching. + */ + private static void promoteForegroundApp( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + String sharingForegroundApp = findSharingForegroundApp(shareTargetMap, dataManager, + callingUserId); + if (sharingForegroundApp != null) { + ShareTargetPredictor.ShareTarget target = shareTargetMap.get(sharingForegroundApp).get( + 0); + target.setScore(probOR(target.getScore(), FOREGROUND_APP_WEIGHT)); + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: promoteForegroundApp packageName: %s, className: %s, " + + "total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getScore())); + } + } + } + + /** + * Find the foreground app just prior to source sharing app from usageStatsManager. Returns null + * if it is not available. + */ + @Nullable + private static String findSharingForegroundApp( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + String sharingForegroundApp = null; + long now = System.currentTimeMillis(); + List<UsageEvents.Event> events = dataManager.queryAppMovingToForegroundEvents( + callingUserId, now - FOREGROUND_APP_PROMO_TIME_WINDOW, now); + String sourceApp = null; + for (int i = events.size() - 1; i >= 0; i--) { + String className = events.get(i).getClassName(); + String packageName = events.get(i).getPackageName(); + if (packageName == null || (className != null && className.contains(CHOOSER_ACTIVITY)) + || packageName.contains(CHOOSER_ACTIVITY)) { + continue; + } + if (sourceApp == null) { + sourceApp = packageName; + } else if (!packageName.equals(sourceApp) && shareTargetMap.containsKey(packageName)) { + sharingForegroundApp = packageName; + break; + } + } + return sharingForegroundApp; + } + + /** + * Probabilistic OR (also known as the algebraic sum). If a <= 1 and b <= 1, the result will be + * <= 1.0. + */ + private static float probOR(float a, float b) { + return 1f - (1f - a) * (1f - b); + } + + /** Counts frequency of share targets. Decays frequency for old shares. */ + private static float getFreqDecayedOnElapsedTime(long elapsedTimeMillis) { + Duration duration = Duration.ofMillis(elapsedTimeMillis); + if (duration.compareTo(Duration.ofDays(1)) <= 0) { + return 1.0f; + } else if (duration.compareTo(Duration.ofDays(3)) <= 0) { + return 0.9f; + } else if (duration.compareTo(Duration.ofDays(7)) <= 0) { + return 0.8f; + } else if (duration.compareTo(Duration.ofDays(14)) <= 0) { + return 0.7f; + } else { + return 0.6f; + } + } + + /** Normalizes frequency score. */ + private static float normalizeFreqScore(double freqRatio) { + if (freqRatio >= 2.5) { + return 0.2f; + } else if (freqRatio >= 1.5) { + return 0.15f; + } else if (freqRatio >= 1.0) { + return 0.1f; + } else if (freqRatio >= 0.75) { + return 0.05f; + } else { + return 0f; + } + } + + /** Normalizes mimetype-specific frequency score. */ + private static float normalizeMimeFreqScore(double freqRatio) { + if (freqRatio >= 2.0) { + return 0.2f; + } else if (freqRatio >= 1.2) { + return 0.15f; + } else if (freqRatio > 0.0) { + return 0.1f; + } else { + return 0f; + } + } + + private static class ShareTargetRankingScore { + + private float mRecencyScore = 0f; + private float mFrequencyScore = 0f; + private float mMimeFrequencyScore = 0f; + private float mTotalScore = 0f; + + float getTotalScore() { + return mTotalScore; + } + + void setTotalScore(float totalScore) { + mTotalScore = totalScore; + } + + float getRecencyScore() { + return mRecencyScore; + } + + void setRecencyScore(float recencyScore) { + mRecencyScore = recencyScore; + } + + float getFrequencyScore() { + return mFrequencyScore; + } + + void setFrequencyScore(float frequencyScore) { + mFrequencyScore = frequencyScore; + } + + void incrementFrequencyScore(float incremental) { + mFrequencyScore += incremental; + } + + float getMimeFrequencyScore() { + return mMimeFrequencyScore; + } + + void setMimeFrequencyScore(float mimeFrequencyScore) { + mMimeFrequencyScore = mimeFrequencyScore; + } + + void incrementMimeFrequencyScore(float incremental) { + mMimeFrequencyScore += incremental; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java index 7934d33f907d..03d9ad51e6c5 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java @@ -21,15 +21,16 @@ import static com.android.server.people.data.TestUtils.timestamp; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.content.Context; import android.content.LocusId; @@ -50,6 +51,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Predicate; @@ -58,7 +60,8 @@ import java.util.function.Predicate; public final class UsageStatsQueryHelperTest { private static final int USER_ID_PRIMARY = 0; - private static final String PKG_NAME = "pkg"; + private static final String PKG_NAME_1 = "pkg_1"; + private static final String PKG_NAME_2 = "pkg_2"; private static final String ACTIVITY_NAME = "TestActivity"; private static final String SHORTCUT_ID = "abc"; private static final LocusId LOCUS_ID_1 = new LocusId("locus_1"); @@ -80,7 +83,7 @@ public final class UsageStatsQueryHelperTest { File testDir = new File(ctx.getCacheDir(), "testdir"); ScheduledExecutorService scheduledExecutorService = new MockScheduledExecutorService(); - mPackageData = new TestPackageData(PKG_NAME, USER_ID_PRIMARY, pkg -> false, pkg -> false, + mPackageData = new TestPackageData(PKG_NAME_1, USER_ID_PRIMARY, pkg -> false, pkg -> false, scheduledExecutorService, testDir); mPackageData.mConversationStore.mConversationInfo = new ConversationInfo.Builder() .setShortcutId(SHORTCUT_ID) @@ -173,10 +176,72 @@ public final class UsageStatsQueryHelperTest { assertEquals(createInAppConversationEvent(130_000L, 30), events.get(2)); } + @Test + public void testQueryAppMovingToForegroundEvents() { + addUsageEvents( + createShortcutInvocationEvent(100_000L), + createActivityResumedEvent(110_000L), + createActivityStoppedEvent(120_000L), + createActivityResumedEvent(130_000L)); + + List<UsageEvents.Event> events = mHelper.queryAppMovingToForegroundEvents(USER_ID_PRIMARY, + 90_000L, + 200_000L); + + assertEquals(2, events.size()); + assertEquals(UsageEvents.Event.ACTIVITY_RESUMED, events.get(0).getEventType()); + assertEquals(110_000L, events.get(0).getTimeStamp()); + assertEquals(UsageEvents.Event.ACTIVITY_RESUMED, events.get(1).getEventType()); + assertEquals(130_000L, events.get(1).getTimeStamp()); + } + + @Test + public void testQueryAppLaunchCount() { + + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), + anyLong(), anyBoolean())).thenReturn( + List.of(packageStats1, packageStats2, packageStats3)); + + Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, + 200_000L, Set.of(PKG_NAME_1, PKG_NAME_2)); + + assertEquals(2, appLaunchCounts.size()); + assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); + assertEquals(1, (long) appLaunchCounts.get(PKG_NAME_2)); + } + + @Test + public void testQueryAppLaunchCount_packageNameFiltered() { + + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), + anyLong(), anyBoolean())).thenReturn( + List.of(packageStats1, packageStats2, packageStats3)); + + Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, + 200_000L, + Set.of(PKG_NAME_1)); + + assertEquals(1, appLaunchCounts.size()); + assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); + } + private void addUsageEvents(UsageEvents.Event... events) { UsageEvents usageEvents = new UsageEvents(Arrays.asList(events), new String[]{}); when(mUsageStatsManagerInternal.queryEventsForUser(anyInt(), anyLong(), anyLong(), - eq(UsageEvents.SHOW_ALL_EVENT_DATA))).thenReturn(usageEvents); + anyInt())).thenReturn(usageEvents); + } + + private static UsageStats createUsageStats(String packageName, int launchCount) { + UsageStats packageStats = new UsageStats(); + packageStats.mPackageName = packageName; + packageStats.mAppLaunchCount = launchCount; + return packageStats; } private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { @@ -203,9 +268,15 @@ public final class UsageStatsQueryHelperTest { return e; } + private static UsageEvents.Event createActivityResumedEvent(long timestamp) { + UsageEvents.Event e = createUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, timestamp); + e.mClass = ACTIVITY_NAME; + return e; + } + private static UsageEvents.Event createUsageEvent(int eventType, long timestamp) { UsageEvents.Event e = new UsageEvents.Event(eventType, timestamp); - e.mPackage = PKG_NAME; + e.mPackage = PKG_NAME_1; return e; } diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java index c6cd34732acf..1480627b9b9f 100644 --- a/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java +++ b/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java @@ -127,6 +127,9 @@ public final class ShareTargetPredictorTest { when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -183,6 +186,12 @@ public final class ShareTargetPredictorTest { when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); when(mEventHistory6.getEventIndex(anySet())).thenReturn(mEventIndex6); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anyInt())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anyInt())).thenReturn(mEventIndex5); + when(mEventHistory6.getEventIndex(anyInt())).thenReturn(mEventIndex6); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -220,19 +229,19 @@ public final class ShareTargetPredictorTest { @Test public void testSortTargets() { AppTarget appTarget1 = new AppTarget.Builder( - new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) .setClassName(CLASS_1) .build(); AppTarget appTarget2 = new AppTarget.Builder( - new AppTargetId("cls2#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + new AppTargetId("cls2#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) .setClassName(CLASS_2) .build(); AppTarget appTarget3 = new AppTarget.Builder( - new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) .setClassName(CLASS_1) .build(); AppTarget appTarget4 = new AppTarget.Builder( - new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) .setClassName(CLASS_2) .build(); AppTarget appTarget5 = new AppTarget.Builder( @@ -251,6 +260,10 @@ public final class ShareTargetPredictorTest { when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anyInt())).thenReturn(mEventIndex4); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -265,14 +278,14 @@ public final class ShareTargetPredictorTest { appTarget4, appTarget3, appTarget2, appTarget1, appTarget5); } - private ShareShortcutInfo buildShareShortcut( + private static ShareShortcutInfo buildShareShortcut( String packageName, String className, String shortcutId) { ShortcutInfo shortcutInfo = buildShortcut(packageName, shortcutId); ComponentName componentName = new ComponentName(packageName, className); return new ShareShortcutInfo(shortcutInfo, componentName); } - private ShortcutInfo buildShortcut(String packageName, String shortcutId) { + private static ShortcutInfo buildShortcut(String packageName, String shortcutId) { Context mockContext = mock(Context.class); when(mockContext.getPackageName()).thenReturn(packageName); when(mockContext.getUserId()).thenReturn(USER_ID); diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java new file mode 100644 index 000000000000..9d96d6b7d861 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 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.people.prediction; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetId; +import android.app.usage.UsageEvents; +import android.os.UserHandle; +import android.util.Range; + +import com.android.server.people.data.DataManager; +import com.android.server.people.data.Event; +import com.android.server.people.data.EventHistory; +import com.android.server.people.data.EventIndex; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +@RunWith(JUnit4.class) +public final class SharesheetModelScorerTest { + + private static final int USER_ID = 0; + private static final String PACKAGE_1 = "pkg1"; + private static final String PACKAGE_2 = "pkg2"; + private static final String PACKAGE_3 = "pkg3"; + private static final String CLASS_1 = "cls1"; + private static final String CLASS_2 = "cls2"; + private static final double DELTA = 1e-6; + private static final long NOW = System.currentTimeMillis(); + private static final Range<Long> WITHIN_ONE_DAY = new Range( + NOW - Duration.ofHours(23).toMillis(), + NOW - Duration.ofHours(22).toMillis()); + private static final Range<Long> TWO_DAYS_AGO = new Range( + NOW - Duration.ofHours(50).toMillis(), + NOW - Duration.ofHours(49).toMillis()); + private static final Range<Long> FIVE_DAYS_AGO = new Range( + NOW - Duration.ofDays(6).toMillis(), + NOW - Duration.ofDays(5).toMillis()); + private static final Range<Long> EIGHT_DAYS_AGO = new Range( + NOW - Duration.ofDays(9).toMillis(), + NOW - Duration.ofDays(8).toMillis()); + private static final Range<Long> TWELVE_DAYS_AGO = new Range( + NOW - Duration.ofDays(13).toMillis(), + NOW - Duration.ofDays(12).toMillis()); + private static final Range<Long> TWENTY_DAYS_AGO = new Range( + NOW - Duration.ofDays(21).toMillis(), + NOW - Duration.ofDays(20).toMillis()); + private static final Range<Long> FOUR_WEEKS_AGO = new Range( + NOW - Duration.ofDays(29).toMillis(), + NOW - Duration.ofDays(28).toMillis()); + + @Mock + private DataManager mDataManager; + @Mock + private EventHistory mEventHistory1; + @Mock + private EventHistory mEventHistory2; + @Mock + private EventHistory mEventHistory3; + @Mock + private EventHistory mEventHistory4; + @Mock + private EventHistory mEventHistory5; + @Mock + private EventIndex mEventIndex1; + @Mock + private EventIndex mEventIndex2; + @Mock + private EventIndex mEventIndex3; + @Mock + private EventIndex mEventIndex4; + @Mock + private EventIndex mEventIndex5; + @Mock + private EventIndex mEventIndex6; + @Mock + private EventIndex mEventIndex7; + @Mock + private EventIndex mEventIndex8; + @Mock + private EventIndex mEventIndex9; + @Mock + private EventIndex mEventIndex10; + + private ShareTargetPredictor.ShareTarget mShareTarget1; + private ShareTargetPredictor.ShareTarget mShareTarget2; + private ShareTargetPredictor.ShareTarget mShareTarget3; + private ShareTargetPredictor.ShareTarget mShareTarget4; + private ShareTargetPredictor.ShareTarget mShareTarget5; + private ShareTargetPredictor.ShareTarget mShareTarget6; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mShareTarget1 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory1, null); + mShareTarget2 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder(new AppTargetId("cls2#pkg1"), PACKAGE_1, + UserHandle.of(USER_ID)).setClassName(CLASS_2).build(), + mEventHistory2, null); + mShareTarget3 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory3, null); + mShareTarget4 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + .setClassName(CLASS_2).build(), + mEventHistory4, null); + mShareTarget5 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg3"), PACKAGE_3, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory5, null); + mShareTarget6 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls2#pkg3"), PACKAGE_3, UserHandle.of(USER_ID)) + .setClassName(CLASS_2).build(), + null, null); + } + + @Test + public void testComputeScore() { + // Frequency and recency + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + + when(mEventIndex1.getActiveTimeSlots()).thenReturn( + List.of(WITHIN_ONE_DAY, TWO_DAYS_AGO, FIVE_DAYS_AGO)); + when(mEventIndex2.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex3.getActiveTimeSlots()).thenReturn(List.of(FIVE_DAYS_AGO, TWENTY_DAYS_AGO)); + when(mEventIndex4.getActiveTimeSlots()).thenReturn( + List.of(EIGHT_DAYS_AGO, TWELVE_DAYS_AGO, FOUR_WEEKS_AGO)); + when(mEventIndex5.getActiveTimeSlots()).thenReturn(List.of()); + + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + + // Frequency of the same mime type + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + + when(mEventIndex6.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO)); + when(mEventIndex7.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex8.getActiveTimeSlots()).thenReturn(List.of()); + when(mEventIndex9.getActiveTimeSlots()).thenReturn(List.of(EIGHT_DAYS_AGO)); + when(mEventIndex10.getActiveTimeSlots()).thenReturn(List.of()); + + SharesheetModelScorer.computeScore( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, + NOW); + + // Verification + assertEquals(0.514f, mShareTarget1.getScore(), DELTA); + assertEquals(0.475125f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.4411f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare() { + // Frequency and recency + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + + when(mEventIndex1.getActiveTimeSlots()).thenReturn( + List.of(WITHIN_ONE_DAY, TWO_DAYS_AGO, FIVE_DAYS_AGO)); + when(mEventIndex2.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex3.getActiveTimeSlots()).thenReturn(List.of(FIVE_DAYS_AGO, TWENTY_DAYS_AGO)); + when(mEventIndex4.getActiveTimeSlots()).thenReturn( + List.of(EIGHT_DAYS_AGO, TWELVE_DAYS_AGO, FOUR_WEEKS_AGO)); + when(mEventIndex5.getActiveTimeSlots()).thenReturn(List.of()); + + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + + // Frequency of the same mime type + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + + when(mEventIndex6.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO)); + when(mEventIndex7.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex8.getActiveTimeSlots()).thenReturn(List.of()); + when(mEventIndex9.getActiveTimeSlots()).thenReturn(List.of(EIGHT_DAYS_AGO)); + when(mEventIndex10.getActiveTimeSlots()).thenReturn(List.of()); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + // Verification + assertEquals(0.514f, mShareTarget1.getScore(), DELTA); + assertEquals(0.475125f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.4411f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_promoteFrequentlyUsedApps() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + .thenReturn( + Map.of(PACKAGE_1, 1, + PACKAGE_2, 2, + PACKAGE_3, 3)); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), + anySet()); + assertEquals(0.9f, mShareTarget5.getScore(), DELTA); + assertEquals(0.81f, mShareTarget3.getScore(), DELTA); + assertEquals(0.729f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_skipPromoteFrequentlyUsedAppsWhenReachesLimit() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + .thenReturn( + Map.of(PACKAGE_1, 1, + PACKAGE_2, 2, + PACKAGE_3, 3)); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 4, NOW, mDataManager, USER_ID); + + verify(mDataManager, never()).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet()); + assertEquals(0.4f, mShareTarget1.getScore(), DELTA); + assertEquals(0.35f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.31f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_promoteForegroundApp() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong())).thenReturn( + List.of(createUsageEvent(PACKAGE_2), + createUsageEvent(PACKAGE_3), + createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3)) + ); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong()); + assertEquals(0f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(SharesheetModelScorer.FOREGROUND_APP_WEIGHT, mShareTarget3.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_skipPromoteForegroundAppWhenNoValidForegroundApp() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong())).thenReturn( + List.of(createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3), + createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3)) + ); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong()); + assertEquals(0f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(0f, mShareTarget3.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + private static UsageEvents.Event createUsageEvent(String packageName) { + UsageEvents.Event e = new UsageEvents.Event(); + e.mPackage = packageName; + return e; + } +} diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index c1447465f53f..991375c5bc73 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -2520,7 +2520,6 @@ public final class SmsManager { * @param sentIntent if not NULL this <code>PendingIntent</code> is * broadcast when the message is successfully sent, or failed * @throws IllegalArgumentException if contentUri is empty - * @deprecated use {@link MmsManager#sendMultimediaMessage} instead. */ public void sendMultimediaMessage(Context context, Uri contentUri, String locationUrl, Bundle configOverrides, PendingIntent sentIntent) { @@ -2555,7 +2554,6 @@ public final class SmsManager { * @param downloadedIntent if not NULL this <code>PendingIntent</code> is * broadcast when the message is downloaded, or the download is failed * @throws IllegalArgumentException if locationUrl or contentUri is empty - * @deprecated use {@link MmsManager#downloadMultimediaMessage} instead. */ public void downloadMultimediaMessage(Context context, String locationUrl, Uri contentUri, Bundle configOverrides, PendingIntent downloadedIntent) { diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java index f444b77b738e..0ad30391d1a8 100644 --- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java +++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java @@ -46,6 +46,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -72,6 +73,7 @@ public class AppLaunch extends InstrumentationTestCase { private static final String KEY_REQUIRED_ACCOUNTS = "required_accounts"; private static final String KEY_APPS = "apps"; private static final String KEY_IORAP_TRIAL_LAUNCH = "iorap_trial_launch"; + private static final String KEY_IORAP_COMPILER_FILTERS = "iorap_compiler_filters"; private static final String KEY_TRIAL_LAUNCH = "trial_launch"; private static final String KEY_LAUNCH_ITERATIONS = "launch_iterations"; private static final String KEY_LAUNCH_ORDER = "launch_order"; @@ -153,6 +155,7 @@ public class AppLaunch extends InstrumentationTestCase { private BufferedWriter mBufferedWriter = null; private boolean mSimplePerfAppOnly = false; private String[] mCompilerFilters = null; + private List<String> mIorapCompilerFilters = null; private String mLastAppName = ""; private boolean mCycleCleanUp = false; private boolean mTraceAll = false; @@ -618,6 +621,24 @@ public class AppLaunch extends InstrumentationTestCase { return reason; } + private boolean shouldIncludeIorap(String compilerFilter) { + if (!mIorapTrialLaunch) { + return false; + } + + // No iorap compiler filters specified: treat all compiler filters as ok. + if (mIorapCompilerFilters == null) { + return true; + } + + // iorap compiler filters specified: the compilerFilter must be in the whitelist. + if (mIorapCompilerFilters.indexOf(compilerFilter) != -1) { + return true; + } + + return false; + } + /** * If launch order is "cyclic" then apps will be launched one after the * other for each iteration count. @@ -632,7 +653,7 @@ public class AppLaunch extends InstrumentationTestCase { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false)); } } - if (mIorapTrialLaunch) { + if (shouldIncludeIorap(compilerFilter)) { for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) { for (String app : mNameToResultKey.keySet()) { String reason = makeReasonForIorapTrialLaunch(launchCount); @@ -646,14 +667,16 @@ public class AppLaunch extends InstrumentationTestCase { for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) { for (String app : mNameToResultKey.keySet()) { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, - String.format(LAUNCH_ITERATION, launchCount), mIorapTrialLaunch)); + String.format(LAUNCH_ITERATION, launchCount), + shouldIncludeIorap(compilerFilter))); } } if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) { for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) { for (String app : mNameToResultKey.keySet()) { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, - String.format(TRACE_ITERATION, traceCount), mIorapTrialLaunch)); + String.format(TRACE_ITERATION, traceCount), + shouldIncludeIorap(compilerFilter))); } } } @@ -664,7 +687,7 @@ public class AppLaunch extends InstrumentationTestCase { if (mTrialLaunch) { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false)); } - if (mIorapTrialLaunch) { + if (shouldIncludeIorap(compilerFilter)) { for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) { String reason = makeReasonForIorapTrialLaunch(launchCount); mLaunchOrderList.add( @@ -675,12 +698,14 @@ public class AppLaunch extends InstrumentationTestCase { } for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, - String.format(LAUNCH_ITERATION, launchCount), mIorapTrialLaunch)); + String.format(LAUNCH_ITERATION, launchCount), + shouldIncludeIorap(compilerFilter))); } if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) { for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) { mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, - String.format(TRACE_ITERATION, traceCount), mIorapTrialLaunch)); + String.format(TRACE_ITERATION, traceCount), + shouldIncludeIorap(compilerFilter))); } } } @@ -822,6 +847,13 @@ public class AppLaunch extends InstrumentationTestCase { mCompilerFilters = new String[1]; } + String iorapCompilerFilterList = args.getString(KEY_IORAP_COMPILER_FILTERS); + if (iorapCompilerFilterList != null) { + // Passing in iorap compiler filters implies an iorap trial launch. + mIorapTrialLaunch = true; + mIorapCompilerFilters = Arrays.asList(iorapCompilerFilterList.split("\\|")); + } + // Pre-populate the results map to avoid null checks. for (String app : mNameToLaunchTime.keySet()) { HashMap<String, List<AppLaunchResult>> map = new HashMap<>(); |