diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 08a09e1b1a73b3af5f3b6ba0fae7f80c4fbf62c6..b1f587e45a6d684c56ff87ac33f8e2bf2bf1e00c 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -334,7 +334,7 @@ java_aconfig_library { aconfig_declarations { name: "android.app.flags-aconfig", package: "android.app", - srcs: ["core/java/android/app/activity_manager.aconfig"], + srcs: ["core/java/android/app/*.aconfig"], } java_aconfig_library { diff --git a/boot/Android.bp b/boot/Android.bp index 93d425e439a9353cb57ede40e809a8944239f0d5..b33fab6e1a9fa40f57e4571d7f4eb078f9455972 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -208,6 +208,13 @@ custom_platform_bootclasspath { ], } +genrule { // This module exists to make the srcjar output available to Make. + name: "platform-bootclasspath.srcjar", + srcs: [":platform-bootclasspath{.srcjar}"], + out: ["platform-bootclasspath.srcjar"], + cmd: "cp $(in) $(out)", +} + platform_systemserverclasspath { name: "platform-systemserverclasspath", } diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md index 82df55509a7ad5bbd0d66ab2a5c73ab72512edd1..e7361fe95e8ed52e65556c7b3c743cbeba891bcb 100644 --- a/cmds/uinput/README.md +++ b/cmds/uinput/README.md @@ -48,6 +48,7 @@ Register a new uinput device | `vid` | 16-bit integer | Vendor ID | | `pid` | 16-bit integer | Product ID | | `bus` | string | Bus that device should use | +| `port` | string | `phys` value to report | | `configuration` | object array | uinput device configuration| | `ff_effects_max` | integer | `ff_effects_max` value | | `abs_info` | array | Absolute axes information | diff --git a/core/api/current.txt b/core/api/current.txt index c9f06397abbf2e26013cf68b51204507562ad08e..43c8214e974973adddee0937c630ce5e991a33db 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5952,6 +5952,7 @@ package android.app { public class GrammaticalInflectionManager { method public int getApplicationGrammaticalGender(); + method @FlaggedApi("android.app.system_terms_of_address_enabled") public int getSystemGrammaticalGender(); method public void setRequestedApplicationGrammaticalGender(int); } @@ -42903,7 +42904,7 @@ package android.telephony { field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array"; field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string"; field public static final String KEY_CARRIER_SETTINGS_ENABLE_BOOL = "carrier_settings_enable_bool"; - field public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE = "carrier_supported_satellite_services_per_provider_bundle"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE = "carrier_supported_satellite_services_per_provider_bundle"; field public static final String KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL = "carrier_supports_opp_data_auto_provisioning_bool"; field public static final String KEY_CARRIER_SUPPORTS_SS_OVER_UT_BOOL = "carrier_supports_ss_over_ut_bool"; field public static final String KEY_CARRIER_SUPPORTS_TETHERING_BOOL = "carrier_supports_tethering_bool"; @@ -43071,6 +43072,7 @@ package android.telephony { field public static final String KEY_RTT_SUPPORTED_WHILE_ROAMING_BOOL = "rtt_supported_while_roaming_bool"; field public static final String KEY_RTT_UPGRADE_SUPPORTED_BOOL = "rtt_upgrade_supported_bool"; field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool"; field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool"; field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1d88e002b4bddc1125457c4b0e1744121409d4ae..98a454ad3eabd9508484da205bb0485b88670599 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16731,17 +16731,22 @@ package android.telephony.satellite { } public final class SatelliteManager { + method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatelliteService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteProvisionStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteProvisionStateCallback); + method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteAttachEnabledForCarrier(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteCommunicationAllowedForCurrentLocation(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method public void requestIsSatelliteSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteAttachEnabledForCarrier(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestTimeForNextSatelliteVisibility(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.time.Duration,android.telephony.satellite.SatelliteManager.SatelliteException>); @@ -16768,6 +16773,7 @@ package android.telephony.satellite { field public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2; // 0x2 field public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4; // 0x4 field public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; // 0x1 field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0 field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7 field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6; // 0x6 diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 25c48e630380cb00dbe58633c04c6439ee7aa77d..183783bdf3096ef358d4452b2bd5da487b83a8da 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -62,7 +62,7 @@ import android.app.servertransaction.ActivityLifecycleItem.LifecycleState; import android.app.servertransaction.ActivityRelaunchItem; import android.app.servertransaction.ActivityResultItem; import android.app.servertransaction.ClientTransaction; -import android.app.servertransaction.ClientTransactionItem; +import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PauseActivityItem; import android.app.servertransaction.PendingTransactionActions; import android.app.servertransaction.PendingTransactionActions.StopInfo; @@ -375,8 +375,8 @@ public final class ActivityThread extends ClientTransactionHandler @GuardedBy("mPendingOverrideConfigs") private final ArrayMap<IBinder, Configuration> mPendingOverrideConfigs = new ArrayMap<>(); /** The activities to be truly destroyed (not include relaunch). */ - final Map<IBinder, ClientTransactionItem> mActivitiesToBeDestroyed = - Collections.synchronizedMap(new ArrayMap<IBinder, ClientTransactionItem>()); + final Map<IBinder, DestroyActivityItem> mActivitiesToBeDestroyed = + Collections.synchronizedMap(new ArrayMap<>()); // List of new activities that should be reported when next we idle. final ArrayList<ActivityClientRecord> mNewActivities = new ArrayList<>(); // Number of activities that are currently visible on-screen. @@ -5799,7 +5799,7 @@ public final class ActivityThread extends ClientTransactionHandler } @Override - public Map<IBinder, ClientTransactionItem> getActivitiesToBeDestroyed() { + public Map<IBinder, DestroyActivityItem> getActivitiesToBeDestroyed() { return mActivitiesToBeDestroyed; } diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 98020ff2d173b6ca8d7764fb362f6b7003001e9b..25075e91088fe1abd8ac28fa74b86ac639b36d41 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -19,7 +19,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread.ActivityClientRecord; import android.app.servertransaction.ClientTransaction; -import android.app.servertransaction.ClientTransactionItem; +import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PendingTransactionActions; import android.app.servertransaction.TransactionExecutor; import android.content.Context; @@ -108,7 +108,7 @@ public abstract class ClientTransactionHandler { // and deliver callbacks. /** Get activity and its corresponding transaction item which are going to destroy. */ - public abstract Map<IBinder, ClientTransactionItem> getActivitiesToBeDestroyed(); + public abstract Map<IBinder, DestroyActivityItem> getActivitiesToBeDestroyed(); /** Destroy the activity. */ public abstract void handleDestroyActivity(@NonNull ActivityClientRecord r, boolean finishing, diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java index 1905b6a46d7e85da0fa0b6225ccf961fe7a425e8..bc6fe614676458a3813421d94df03268fb6beb5a 100644 --- a/core/java/android/app/GrammaticalInflectionManager.java +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -16,12 +16,15 @@ package android.app; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.SystemService; import android.content.Context; import android.content.res.Configuration; import android.os.RemoteException; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -31,11 +34,15 @@ import java.util.Set; */ @SystemService(Context.GRAMMATICAL_INFLECTION_SERVICE) public class GrammaticalInflectionManager { - private static final Set<Integer> VALID_GENDER_VALUES = new HashSet<>(Arrays.asList( + + /** @hide */ + @NonNull + public static final Set<Integer> VALID_GRAMMATICAL_GENDER_VALUES = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED, Configuration.GRAMMATICAL_GENDER_NEUTRAL, Configuration.GRAMMATICAL_GENDER_FEMININE, - Configuration.GRAMMATICAL_GENDER_MASCULINE)); + Configuration.GRAMMATICAL_GENDER_MASCULINE))); private final Context mContext; private final IGrammaticalInflectionManager mService; @@ -79,7 +86,7 @@ public class GrammaticalInflectionManager { */ public void setRequestedApplicationGrammaticalGender( @Configuration.GrammaticalGender int grammaticalGender) { - if (!VALID_GENDER_VALUES.contains(grammaticalGender)) { + if (!VALID_GRAMMATICAL_GENDER_VALUES.contains(grammaticalGender)) { throw new IllegalArgumentException("Unknown grammatical gender"); } @@ -90,4 +97,44 @@ public class GrammaticalInflectionManager { throw e.rethrowFromSystemServer(); } } + + /** + * Sets the current grammatical gender for all privileged applications. The value will be + * stored in an encrypted file at {@link android.os.Environment#getDataSystemCeDirectory(int) + * + * @param grammaticalGender the terms of address the user preferred in system. + * + * @see Configuration#getGrammaticalGender + * @hide + */ + public void setSystemWideGrammaticalGender( + @Configuration.GrammaticalGender int grammaticalGender) { + if (!VALID_GRAMMATICAL_GENDER_VALUES.contains(grammaticalGender)) { + throw new IllegalArgumentException("Unknown grammatical gender"); + } + + try { + mService.setSystemWideGrammaticalGender(mContext.getUserId(), grammaticalGender); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the current grammatical gender of privileged application from the encrypted file, + * which is stored under {@link android.os.Environment#getDataSystemCeDirectory(int)}. + * + * @return the value of grammatical gender + * + * @see Configuration#getGrammaticalGender + */ + @FlaggedApi(Flags.FLAG_SYSTEM_TERMS_OF_ADDRESS_ENABLED) + @Configuration.GrammaticalGender + public int getSystemGrammaticalGender() { + try { + return mService.getSystemGrammaticalGender(mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl index 9366a45551da1bac94e268fd25ccc4e98b909a02..48a48416d59217c0aa47f427981d0f5af22a23a5 100644 --- a/core/java/android/app/IGrammaticalInflectionManager.aidl +++ b/core/java/android/app/IGrammaticalInflectionManager.aidl @@ -16,4 +16,14 @@ package android.app; * Sets a specified app’s app-specific grammatical gender. */ void setRequestedApplicationGrammaticalGender(String appPackageName, int userId, int gender); - } \ No newline at end of file + + /** + * Sets the grammatical gender to system. + */ + void setSystemWideGrammaticalGender(int userId, int gender); + + /** + * Gets the grammatical gender from system. + */ + int getSystemGrammaticalGender(int userId); + } diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index e1c45d98e6787c36f29880db7660bf359f17ab33..9cf54e3b906367d2911fb2fd88b7c1479c24d691 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -54,6 +54,9 @@ per-file IBackupAgent.aidl = file:/services/backup/OWNERS per-file Broadcast* = file:/BROADCASTS_OWNERS per-file ReceiverInfo* = file:/BROADCASTS_OWNERS +# GrammaticalInflectionManager +per-file *GrammaticalInflection* = file:/services/core/java/com/android/server/grammaticalinflection/OWNERS + # KeyguardManager per-file KeyguardManager.java = file:/services/core/java/com/android/server/locksettings/OWNERS diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING index 315a0556042abf2b3983a81ac5ab58c28cafdd96..a29c196d88de21a0a3f1fd915415b2cdced1d92e 100644 --- a/core/java/android/app/TEST_MAPPING +++ b/core/java/android/app/TEST_MAPPING @@ -19,7 +19,7 @@ "name": "CtsAppOpsTestCases", "options": [ { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] }, diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig new file mode 100644 index 0000000000000000000000000000000000000000..989ce61337a3bfa647510f89081e11b02ac60094 --- /dev/null +++ b/core/java/android/app/grammatical_inflection_manager.aconfig @@ -0,0 +1,8 @@ +package: "android.app" + +flag { + name: "system_terms_of_address_enabled" + namespace: "grammatical_gender" + description: "Feature flag for System Terms of Address" + bug: "297798866" +} diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java index a5b0f18dad3b19cb97cf957bbec12218c1000306..8617386516afb7ccc88baccb39104870fdb04dc5 100644 --- a/core/java/android/app/servertransaction/ClientTransaction.java +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.app.ClientTransactionHandler; import android.app.IApplicationThread; import android.compat.annotation.UnsupportedAppUsage; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -83,23 +82,6 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { return mActivityCallbacks; } - /** Get the target activity. */ - @Nullable - @UnsupportedAppUsage - public IBinder getActivityToken() { - // TODO(b/260873529): remove after we allow multiple activity items in one transaction. - if (mLifecycleStateRequest != null) { - return mLifecycleStateRequest.getActivityToken(); - } - for (int i = mActivityCallbacks.size() - 1; i >= 0; i--) { - final IBinder token = mActivityCallbacks.get(i).getActivityToken(); - if (token != null) { - return token; - } - } - return null; - } - /** Get the target state lifecycle request. */ @VisibleForTesting(visibility = PACKAGE) @UnsupportedAppUsage diff --git a/core/java/android/app/servertransaction/DestroyActivityItem.java b/core/java/android/app/servertransaction/DestroyActivityItem.java index ddb6df10517cfe6c79d007eacd44a3a244c43c1d..f9cf075d60624c094de6b4947f0459b368232e09 100644 --- a/core/java/android/app/servertransaction/DestroyActivityItem.java +++ b/core/java/android/app/servertransaction/DestroyActivityItem.java @@ -49,6 +49,13 @@ public class DestroyActivityItem extends ActivityLifecycleItem { Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + @Override + public void postExecute(@NonNull ClientTransactionHandler client, + @NonNull PendingTransactionActions pendingActions) { + // Cleanup after execution. + client.getActivitiesToBeDestroyed().remove(getActivityToken()); + } + @Override public int getTargetState() { return ON_DESTROY; diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index 44336735254df606abee2cf5f5ca9eb1eb5f4774..066f9fe8497042505b74134346e05648d0ab9447 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -47,7 +47,6 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import java.util.List; -import java.util.Map; /** * Class that manages transaction execution in the correct order. @@ -75,34 +74,14 @@ public class TransactionExecutor { * either remain in the initial state, or last state needed by a callback. */ public void execute(@NonNull ClientTransaction transaction) { - if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Start resolving transaction"); - - final IBinder token = transaction.getActivityToken(); - if (token != null) { - final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed = - mTransactionHandler.getActivitiesToBeDestroyed(); - final ClientTransactionItem destroyItem = activitiesToBeDestroyed.get(token); - if (destroyItem != null) { - if (transaction.getLifecycleStateRequest() == destroyItem) { - // It is going to execute the transaction that will destroy activity with the - // token, so the corresponding to-be-destroyed record can be removed. - activitiesToBeDestroyed.remove(token); - } - if (mTransactionHandler.getActivityClient(token) == null) { - // The activity has not been created but has been requested to destroy, so all - // transactions for the token are just like being cancelled. - Slog.w(TAG, tId(transaction) + "Skip pre-destroyed transaction:\n" - + transactionToString(transaction, mTransactionHandler)); - return; - } - } + if (DEBUG_RESOLVER) { + Slog.d(TAG, tId(transaction) + "Start resolving transaction"); + Slog.d(TAG, transactionToString(transaction, mTransactionHandler)); } - if (DEBUG_RESOLVER) Slog.d(TAG, transactionToString(transaction, mTransactionHandler)); - executeCallbacks(transaction); - executeLifecycleState(transaction); + mPendingActions.clear(); if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction"); } @@ -135,6 +114,14 @@ public class TransactionExecutor { final IBinder token = item.getActivityToken(); ActivityClientRecord r = mTransactionHandler.getActivityClient(token); + if (token != null && r == null + && mTransactionHandler.getActivitiesToBeDestroyed().containsKey(token)) { + // The activity has not been created but has been requested to destroy, so all + // transactions for the token are just like being cancelled. + Slog.w(TAG, "Skip pre-destroyed transaction item:\n" + item); + continue; + } + if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item); final int postExecutionState = item.getPostExecutionState(); @@ -211,6 +198,10 @@ public class TransactionExecutor { } if (r == null) { + if (mTransactionHandler.getActivitiesToBeDestroyed().get(token) == lifecycleItem) { + // Always cleanup for destroy item. + lifecycleItem.postExecute(mTransactionHandler, mPendingActions); + } // Ignore requests for non-existent client records for now. return; } diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING index ab669cc141f1abe4660cb90c306b40dfa1d302b5..b0ab11f488580ea7aa7782e9b45b7099839615e1 100644 --- a/core/java/android/content/pm/TEST_MAPPING +++ b/core/java/android/content/pm/TEST_MAPPING @@ -45,7 +45,8 @@ ] }, { - "name":"CarrierAppIntegrationTestCases" + "name":"CarrierAppIntegrationTestCases", + "keywords": ["internal"] }, { "name":"CtsSilentUpdateHostTestCases" diff --git a/core/java/android/net/metrics/WakeupEvent.java b/core/java/android/net/metrics/WakeupEvent.java index af9a73ca31ee50fde5fe00d49f3bedbbfbb61750..53a3ea516530f21ed578fdb3284c9b98d3404486 100644 --- a/core/java/android/net/metrics/WakeupEvent.java +++ b/core/java/android/net/metrics/WakeupEvent.java @@ -29,7 +29,7 @@ public class WakeupEvent { public String iface; public int uid; public int ethertype; - public MacAddress dstHwAddr; + public MacAddress dstHwAddr; // actually used to store a src mac address public String srcIp; public String dstIp; public int ipNextHeader; @@ -44,7 +44,7 @@ public class WakeupEvent { j.add(iface); j.add("uid: " + Integer.toString(uid)); j.add("eth=0x" + Integer.toHexString(ethertype)); - j.add("dstHw=" + dstHwAddr); + j.add("srcMac=" + dstHwAddr); // really!! http://b/292404319#comment11 if (ipNextHeader > 0) { j.add("ipNxtHdr=" + ipNextHeader); j.add("srcIp=" + srcIp); diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index ad3abd9b531c181d2518db94f55db4c320fb43a0..2d6e09a1b268eadb5e0ebf6f1c9f8643b023c1fe 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -8,7 +8,6 @@ "name": "FrameworksVibratorCoreTests", "options": [ {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"}, {"exclude-annotation": "org.junit.Ignore"} ] @@ -21,7 +20,6 @@ "name": "FrameworksVibratorServicesTests", "options": [ {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"}, {"exclude-annotation": "org.junit.Ignore"} ] @@ -34,7 +32,6 @@ "name": "CtsVibratorTestCases", "options": [ {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"}, {"exclude-annotation": "org.junit.Ignore"} ] diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 80b7d40c14c513010c22a82413aa674e02ea2c3e..4c8ef97a7437a868b5f7a5dfb811e9f01a312b17 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2940,6 +2940,12 @@ public class UserManager { * Used to check if the context user is a restricted profile. Restricted profiles * may have a reduced number of available apps, app restrictions, and account restrictions. * + * <p>The caller must be in the same profile group as the context user or else hold + * <li>{@link android.Manifest.permission#MANAGE_USERS}, + * <li>or {@link android.Manifest.permission#CREATE_USERS}, + * <li>or, for devices after {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * {@link android.Manifest.permission#QUERY_USERS}. + * * @return whether the context user is a restricted profile. * @hide */ @@ -2963,6 +2969,12 @@ public class UserManager { * Check if a user is a restricted profile. Restricted profiles may have a reduced number of * available apps, app restrictions, and account restrictions. * + * <p>Requires + * <li>{@link android.Manifest.permission#MANAGE_USERS}, + * <li>or {@link android.Manifest.permission#CREATE_USERS}, + * <li>or, for devices after {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * {@link android.Manifest.permission#QUERY_USERS}. + * * @param user the user to check * @return whether the user is a restricted profile. * @hide diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 77be5d4008536f3f2638cc98c98290eeda18008c..1294f983b3b9368f0e7f06e04444d2cf27a9b173 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -6,3 +6,10 @@ flag { description: "enable device aware permission APIs" bug: "274852670" } + +flag { + name: "voice_activation_permission_apis" + namespace: "permissions" + description: "enable voice activation permission APIs" + bug: "287264308" +} \ No newline at end of file diff --git a/core/java/android/service/notification/TEST_MAPPING b/core/java/android/service/notification/TEST_MAPPING index 7b8d52f51cee7699a0ddb755fc0bb4f1113c1eae..468c4518602eea4c1dd61b8dc7a08967007592fc 100644 --- a/core/java/android/service/notification/TEST_MAPPING +++ b/core/java/android/service/notification/TEST_MAPPING @@ -3,9 +3,6 @@ { "name": "CtsNotificationTestCases", "options": [ - { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" - }, { "exclude-annotation": "androidx.test.filters.FlakyTest" }, @@ -20,9 +17,6 @@ { "name": "FrameworksUiServicesTests", "options": [ - { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" - }, { "exclude-annotation": "androidx.test.filters.FlakyTest" }, diff --git a/core/java/android/text/TEST_MAPPING b/core/java/android/text/TEST_MAPPING index 0fe974afd15c572c3efcde7e5fc2ea677fec3789..c9bd2cacb138457710c1dbe29d2f7e5aab9d4fd1 100644 --- a/core/java/android/text/TEST_MAPPING +++ b/core/java/android/text/TEST_MAPPING @@ -4,7 +4,7 @@ "name": "CtsTextTestCases", "options": [ { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" + "exclude-annotation": "androidx.test.filters.FlakyTest" }, { "exclude-annotation": "androidx.test.filters.LargeTest" diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl index d25c8a834c7bc2ef3eeaaa3c398d0ebd7f9678bb..7b7e34172fed4d129edc7a7a39aae73009dee3a9 100644 --- a/core/java/android/window/ITaskFragmentOrganizerController.aidl +++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl @@ -25,9 +25,12 @@ import android.window.WindowContainerTransaction; interface ITaskFragmentOrganizerController { /** - * Registers a TaskFragmentOrganizer to manage TaskFragments. + * Registers a TaskFragmentOrganizer to manage TaskFragments. Registering a system + * organizer requires MANAGE_ACTIVITY_TASKS permission, and the organizer will have additional + * system capabilities. */ - void registerOrganizer(in ITaskFragmentOrganizer organizer); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value=android.Manifest.permission.MANAGE_ACTIVITY_TASKS, conditional=true)") + void registerOrganizer(in ITaskFragmentOrganizer organizer, in boolean isSystemOrganizer); /** * Unregisters a previously registered TaskFragmentOrganizer. diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index f785a3d1514e3a1b7a848d681c957714b5768919..a6c9cecb508fc5ca4f0838eff3bbbfbe396242eb 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -22,9 +22,11 @@ import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import android.annotation.CallSuper; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.os.Bundle; import android.os.IBinder; @@ -32,6 +34,8 @@ import android.os.RemoteException; import android.view.RemoteAnimationDefinition; import android.view.WindowManager; +import com.android.window.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; @@ -140,12 +144,34 @@ public class TaskFragmentOrganizer extends WindowOrganizer { } /** - * Registers a TaskFragmentOrganizer to manage TaskFragments. + * Registers a {@link TaskFragmentOrganizer} to manage TaskFragments. */ @CallSuper public void registerOrganizer() { + // TODO(b/302420256) point to registerOrganizer(boolean) when flag is removed. + try { + getController().registerOrganizer(mInterface, false /* isSystemOrganizer */); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a {@link TaskFragmentOrganizer} to manage TaskFragments. + * + * Registering a system organizer requires MANAGE_ACTIVITY_TASKS permission, and the organizer + * will have additional system capabilities, including: (1) it will receive SurfaceControl for + * the organized TaskFragment, and (2) it needs to update the + * {@link android.view.SurfaceControl} following the window change accordingly. + * + * @hide + */ + @CallSuper + @RequiresPermission(value = "android.permission.MANAGE_ACTIVITY_TASKS", conditional = true) + @FlaggedApi(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG) + public void registerOrganizer(boolean isSystemOrganizer) { try { - getController().registerOrganizer(mInterface); + getController().registerOrganizer(mInterface, isSystemOrganizer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java index 3c5d60dc8ae26b72791bb8da24fe7fd9f228137f..4dada108c4c64b1e4454c623f1e8b3c5499f233f 100644 --- a/core/java/android/window/TaskFragmentTransaction.java +++ b/core/java/android/window/TaskFragmentTransaction.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.view.SurfaceControl; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -192,6 +193,9 @@ public final class TaskFragmentTransaction implements Parcelable { @Nullable private TaskFragmentParentInfo mTaskFragmentParentInfo; + @Nullable + private SurfaceControl mSurfaceControl; + public Change(@ChangeType int type) { mType = type; } @@ -206,6 +210,7 @@ public final class TaskFragmentTransaction implements Parcelable { mActivityIntent = in.readTypedObject(Intent.CREATOR); mActivityToken = in.readStrongBinder(); mTaskFragmentParentInfo = in.readTypedObject(TaskFragmentParentInfo.CREATOR); + mSurfaceControl = in.readTypedObject(SurfaceControl.CREATOR); } @Override @@ -219,6 +224,7 @@ public final class TaskFragmentTransaction implements Parcelable { dest.writeTypedObject(mActivityIntent, flags); dest.writeStrongBinder(mActivityToken); dest.writeTypedObject(mTaskFragmentParentInfo, flags); + dest.writeTypedObject(mSurfaceControl, flags); } /** The change is related to the TaskFragment created with this unique token. */ @@ -306,6 +312,13 @@ public final class TaskFragmentTransaction implements Parcelable { return this; } + /** @hide */ + @NonNull + public Change setTaskFragmentSurfaceControl(@Nullable SurfaceControl sc) { + mSurfaceControl = sc; + return this; + } + @ChangeType public int getType() { return mType; @@ -359,6 +372,21 @@ public final class TaskFragmentTransaction implements Parcelable { return mTaskFragmentParentInfo; } + /** + * Gets the {@link SurfaceControl} of the TaskFragment. This field is {@code null} for + * a regular {@link TaskFragmentOrganizer} and is only available for a system + * {@link TaskFragmentOrganizer} in the + * {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_APPEARED} event. See + * {@link ITaskFragmentOrganizerController#registerOrganizer(ITaskFragmentOrganizer, + * boolean)} + * + * @hide + */ + @Nullable + public SurfaceControl getTaskFragmentSurfaceControl() { + return mSurfaceControl; + } + @Override public String toString() { return "Change{ type=" + mType + " }"; diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java index 7a87c3a82e43809cc1f1ff309275d94e31ddef3c..d503904c2e3ca6f7036eeda4a9768b727851197d 100644 --- a/core/java/com/android/internal/display/BrightnessSynchronizer.java +++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java @@ -16,11 +16,9 @@ package com.android.internal.display; -import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; -import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.net.Uri; @@ -56,7 +54,8 @@ public class BrightnessSynchronizer { private static final int MSG_RUN_UPDATE = 1; // The tolerance within which we consider brightness values approximately equal to eachother. - public static final float EPSILON = 0.0001f; + // This value is approximately 1/3 of the smallest possible brightness value. + public static final float EPSILON = 0.001f; private static int sBrightnessUpdateCount = 1; @@ -284,74 +283,6 @@ public class BrightnessSynchronizer { } } - /** - * Converts between the int brightness setting and the float brightness system. The int - * brightness setting is between 0-255 and matches the brightness slider - e.g. 128 is 50% on - * the slider. Accounts for special values such as OFF and invalid values. Accounts for - * brightness limits; the maximum value here represents the max value allowed on the slider. - */ - @VisibleForTesting - @SuppressLint("AndroidFrameworkRequiresPermission") - public float brightnessIntSettingToFloat(int brightnessInt) { - if (brightnessInt == PowerManager.BRIGHTNESS_OFF) { - return PowerManager.BRIGHTNESS_OFF_FLOAT; - } else if (brightnessInt == PowerManager.BRIGHTNESS_INVALID) { - return PowerManager.BRIGHTNESS_INVALID_FLOAT; - } else { - final float minInt = PowerManager.BRIGHTNESS_OFF + 1; - final float maxInt = PowerManager.BRIGHTNESS_ON; - - // Normalize to the range [0, 1] - float userPerceptionBrightness = MathUtils.norm(minInt, maxInt, brightnessInt); - - // Convert from user-perception to linear scale - float linearBrightness = BrightnessUtils.convertGammaToLinear(userPerceptionBrightness); - - // Interpolate to the range [0, currentlyAllowedMax] - final Display display = mContext.getDisplay(); - if (display == null) { - return PowerManager.BRIGHTNESS_INVALID_FLOAT; - } - final BrightnessInfo info = display.getBrightnessInfo(); - return MathUtils.lerp(info.brightnessMinimum, info.brightnessMaximum, linearBrightness); - } - } - - /** - * Translates specified value from the float brightness system to the setting int brightness - * system. The value returned is between 0-255 and matches the brightness slider - e.g. 128 is - * 50% on the slider. Accounts for special values such as OFF and invalid values. Accounts for - * brightness limits; the maximum value here represents the max value currently allowed on - * the slider. - */ - @VisibleForTesting - @SuppressLint("AndroidFrameworkRequiresPermission") - public int brightnessFloatToIntSetting(float brightnessFloat) { - if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) { - return PowerManager.BRIGHTNESS_OFF; - } else if (Float.isNaN(brightnessFloat)) { - return PowerManager.BRIGHTNESS_INVALID; - } else { - // Normalize to the range [0, 1] - final Display display = mContext.getDisplay(); - if (display == null) { - return PowerManager.BRIGHTNESS_INVALID; - } - final BrightnessInfo info = display.getBrightnessInfo(); - float linearBrightness = - MathUtils.norm(info.brightnessMinimum, info.brightnessMaximum, brightnessFloat); - - // Convert from linear to user-perception scale - float userPerceptionBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness); - - // Interpolate to the range [0, 255] - final float minInt = PowerManager.BRIGHTNESS_OFF + 1; - final float maxInt = PowerManager.BRIGHTNESS_ON; - float intBrightness = MathUtils.lerp(minInt, maxInt, userPerceptionBrightness); - return Math.round(intBrightness); - } - } - /** * Encapsulates a brightness change event and contains logic for synchronizing the appropriate * settings for the specified brightness change. @@ -490,14 +421,14 @@ public class BrightnessSynchronizer { if (mSourceType == TYPE_INT) { return (int) mBrightness; } - return brightnessFloatToIntSetting(mBrightness); + return brightnessFloatToInt(mBrightness); } private float getBrightnessAsFloat() { if (mSourceType == TYPE_FLOAT) { return mBrightness; } - return brightnessIntSettingToFloat((int) mBrightness); + return brightnessIntToFloat((int) mBrightness); } private String toStringLabel(int type, float brightness) { diff --git a/core/java/com/android/internal/infra/TEST_MAPPING b/core/java/com/android/internal/infra/TEST_MAPPING index ddfd0ed18bef7b7fe16eb0a379f6e0c7bc052010..c09181f2f49640b9c1ec803c19b287607ebb962d 100644 --- a/core/java/com/android/internal/infra/TEST_MAPPING +++ b/core/java/com/android/internal/infra/TEST_MAPPING @@ -4,7 +4,7 @@ "name": "CtsRoleTestCases", "options": [ { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] }, @@ -15,7 +15,7 @@ "include-filter": "android.permission.cts.PermissionControllerTest" }, { - "exclude-annotation": "android.platform.test.annotations.FlakyTest" + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] }, diff --git a/core/java/com/android/internal/net/OWNERS b/core/java/com/android/internal/net/OWNERS index 71f997bb57c58c25229476476f4b2915fd125c8b..71576837c2e12cb647a85fde601d7a4cd6f10b65 100644 --- a/core/java/com/android/internal/net/OWNERS +++ b/core/java/com/android/internal/net/OWNERS @@ -1,4 +1,4 @@ set noparent -file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking jsharkey@android.com diff --git a/core/java/com/android/server/net/OWNERS b/core/java/com/android/server/net/OWNERS index 62c5737a2e8e5a7da8dcff019eb36d3c55b94ce4..c24680e9b06a616db26148e4d2ce2e833626daf9 100644 --- a/core/java/com/android/server/net/OWNERS +++ b/core/java/com/android/server/net/OWNERS @@ -1,2 +1,2 @@ set noparent -file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index c00a776fc3da1749749f69eda3527948baf6a769..4e073ca3863e4c6fb839b4177ef84391c225c25a 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4830,7 +4830,7 @@ <string translatable="false" name="config_deviceSpecificInputMethodManagerService"></string> <!-- Component name of media projection permission dialog --> - <string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity</string> + <string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.mediaprojection.permission.MediaProjectionPermissionActivity</string> <!-- Corner radius of system dialogs --> <dimen name="config_dialogCornerRadius">28dp</dimen> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 878e6b306571427241095957744f20dbd0c96248..71d696ea45548ed40f9c9d9de4c04fedfd9fb509 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -172,28 +172,17 @@ <integer name="config_satellite_nb_iot_inactivity_timeout_millis">180000</integer> <java-symbol type="integer" name="config_satellite_nb_iot_inactivity_timeout_millis" /> - <!-- Telephony config for services supported by satellite providers. The format of each config - string in the array is as follows: "PLMN_1:service_1,service_2,..." - where PLMN is the satellite PLMN of a provider and service is an integer with the - following value: - 1 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VOICE} - 2 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_DATA} - 3 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_SMS} - 4 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VIDEO} - 5 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_EMERGENCY} - 6 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_MMS} - Example of a config string: "10011:2,3" - - The PLMNs not configured in this array will be ignored and will not be used for satellite - scanning. --> - <string-array name="config_satellite_services_supported_by_providers" translatable="false"> - </string-array> - <java-symbol type="array" name="config_satellite_services_supported_by_providers" /> + <!-- Telephony config for the PLMNs of all satellite providers. This is used by satellite modem + to identify providers that should be ignored if the carrier config + carrier_supported_satellite_services_per_provider_bundle does not support them. + --> + <string-array name="config_satellite_providers" translatable="false"></string-array> + <java-symbol type="array" name="config_satellite_providers" /> - <!-- The identifier of the satellite's eSIM profile preloaded on the device. The identifier is - composed of MCC and MNC of the satellite PLMN with the format "mccmnc". --> - <string name="config_satellite_esim_identifier" translatable="false"></string> - <java-symbol type="string" name="config_satellite_esim_identifier" /> + <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC + of the satellite PLMN with the format "mccmnc". --> + <string name="config_satellite_sim_identifier" translatable="false"></string> + <java-symbol type="string" name="config_satellite_sim_identifier" /> <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks will not perform handover if the target transport is out of service, or VoPS not diff --git a/core/tests/coretests/src/android/app/servertransaction/DestroyActivityItemTest.java b/core/tests/coretests/src/android/app/servertransaction/DestroyActivityItemTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ecd75a8370ce44ff9fe252b44a38ca7bf8b0cf64 --- /dev/null +++ b/core/tests/coretests/src/android/app/servertransaction/DestroyActivityItemTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.app.ActivityThread.ActivityClientRecord; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link DestroyActivityItem}. + * + * Build/Install/Run: + * atest FrameworksCoreTests:DestroyActivityItemTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class DestroyActivityItemTest { + + @Mock + private ClientTransactionHandler mHandler; + @Mock + private PendingTransactionActions mPendingActions; + @Mock + private IBinder mActivityToken; + + // Can't mock final class. + private ActivityClientRecord mActivityClientRecord; + + private ArrayMap<IBinder, DestroyActivityItem> mActivitiesToBeDestroyed; + private DestroyActivityItem mItem; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mItem = DestroyActivityItem.obtain( + mActivityToken, false /* finished */, 123 /* configChanges */); + mActivityClientRecord = new ActivityClientRecord(); + mActivitiesToBeDestroyed = new ArrayMap<>(); + + doReturn(mActivitiesToBeDestroyed).when(mHandler).getActivitiesToBeDestroyed(); + } + + @Test + public void testPreExecute() { + mItem.preExecute(mHandler); + + assertEquals(1, mActivitiesToBeDestroyed.size()); + assertEquals(mItem, mActivitiesToBeDestroyed.get(mActivityToken)); + } + + @Test + public void testPostExecute() { + mItem.preExecute(mHandler); + mItem.postExecute(mHandler, mPendingActions); + + assertTrue(mActivitiesToBeDestroyed.isEmpty()); + } + + @Test + public void testExecute() { + mItem.execute(mHandler, mActivityClientRecord, mPendingActions); + + verify(mHandler).handleDestroyActivity(eq(mActivityClientRecord), eq(false) /* finishing */, + eq(123) /* configChanges */, eq(false) /* getNonConfigInstance */, any()); + } +} diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java index a1a2bdbe0f151d07acf7806350ac638c102cfe59..44a4d580dbc0339939d6be5056dbd83194207c0c 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java @@ -28,6 +28,7 @@ import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -247,7 +248,7 @@ public class TransactionExecutorTests { @Test public void testDoNotLaunchDestroyedActivity() { - final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed = new ArrayMap<>(); + final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>(); when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed); // Assume launch transaction is still in queue, so there is no client record. when(mTransactionHandler.getActivityClient(any())).thenReturn(null); @@ -259,7 +260,7 @@ public class TransactionExecutorTests { DestroyActivityItem.obtain(token, false /* finished */, 0 /* configChanges */)); destroyTransaction.preExecute(mTransactionHandler); // The activity should be added to to-be-destroyed container. - assertEquals(1, mTransactionHandler.getActivitiesToBeDestroyed().size()); + assertEquals(1, activitiesToBeDestroyed.size()); // A previous queued launch transaction runs on main thread (execute). final ClientTransaction launchTransaction = ClientTransaction.obtain(null /* client */); @@ -274,7 +275,7 @@ public class TransactionExecutorTests { // After the destroy transaction has been executed, the token should be removed. mExecutor.execute(destroyTransaction); - assertEquals(0, mTransactionHandler.getActivitiesToBeDestroyed().size()); + assertTrue(activitiesToBeDestroyed.isEmpty()); } @Test diff --git a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java index a8b40325a71381c8edf12bfa88be0a01184021f6..a5bbeb58bc0894c8f23fa6fd24168cc87340ecc9 100644 --- a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java +++ b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java @@ -17,6 +17,7 @@ package android.window.flags; import static com.android.window.flags.Flags.syncWindowConfigUpdateFlag; +import static com.android.window.flags.Flags.taskFragmentSystemOrganizerFlag; import android.platform.test.annotations.Presubmit; @@ -42,4 +43,10 @@ public class WindowFlagsTest { // No crash when accessing the flag. syncWindowConfigUpdateFlag(); } + + @Test + public void testTaskFragmentSystemOrganizerFlag() { + // No crash when accessing the flag. + taskFragmentSystemOrganizerFlag(); + } } diff --git a/core/tests/vibrator/TEST_MAPPING b/core/tests/vibrator/TEST_MAPPING index 2f3afa6f63996a9b5ff99ecc54f149124495ad13..54a5ff1d675d0b22f5acb14ef37b7d5eb4fb33b6 100644 --- a/core/tests/vibrator/TEST_MAPPING +++ b/core/tests/vibrator/TEST_MAPPING @@ -4,7 +4,6 @@ "name": "FrameworksVibratorCoreTests", "options": [ {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"}, {"exclude-annotation": "org.junit.Ignore"} ] diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 3d825f072a6aa38237661c72b79db0fc9dcd5698..4ea14f473c390376f3805ae0881c640189379596 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -720,6 +720,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(snapPosition); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, + false /* reparentLeafTaskIfRelaunch */); setRootForceTranslucent(false, wct); // Make sure the launch options will put tasks in the corresponding split roots @@ -767,6 +769,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(snapPosition); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, + false /* reparentLeafTaskIfRelaunch */); setRootForceTranslucent(false, wct); options1 = options1 != null ? options1 : new Bundle(); diff --git a/libs/hwui/jni/Gainmap.cpp b/libs/hwui/jni/Gainmap.cpp index cec0ee7ee247a9d11899b3a69f406f4441a99a25..0fffee744be0564ffbb0b6686d3bb06805249010 100644 --- a/libs/hwui/jni/Gainmap.cpp +++ b/libs/hwui/jni/Gainmap.cpp @@ -208,8 +208,6 @@ static void Gainmap_writeToParcel(JNIEnv* env, jobject, jlong nativeObject, jobj p.writeFloat(info.fDisplayRatioHdr); // base image type p.writeInt32(static_cast<int32_t>(info.fBaseImageType)); - // type - p.writeInt32(static_cast<int32_t>(info.fType)); #else doThrowRE(env, "Cannot use parcels outside of Android!"); #endif @@ -232,7 +230,6 @@ static void Gainmap_readFromParcel(JNIEnv* env, jobject, jlong nativeObject, job info.fDisplayRatioSdr = p.readFloat(); info.fDisplayRatioHdr = p.readFloat(); info.fBaseImageType = static_cast<SkGainmapInfo::BaseImageType>(p.readInt32()); - info.fType = static_cast<SkGainmapInfo::Type>(p.readInt32()); fromJava(nativeObject)->info = info; #else diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index b0cdb0554c11b3bb500fb47e4603647453cf23b4..230fb07f9f43ce4fd4e36328dc51ed35e001a2d3 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -1167,7 +1167,10 @@ public final class AudioAttributes implements Parcelable { streamType); if (attributes != null) { mUsage = attributes.mUsage; - mContentType = attributes.mContentType; + // on purpose ignoring the content type: stream types are deprecated for + // playback, making assumptions about the content type is prone to + // interpretation errors for ambiguous types such as STREAM_TTS and STREAM_MUSIC + //mContentType = attributes.mContentType; mFlags = attributes.getAllFlags(); mMuteHapticChannels = attributes.areHapticChannelsMuted(); mIsContentSpatialized = attributes.isContentSpatialized(); @@ -1177,49 +1180,47 @@ public final class AudioAttributes implements Parcelable { mSource = attributes.mSource; } } - if (mContentType == CONTENT_TYPE_UNKNOWN) { - switch (streamType) { - case AudioSystem.STREAM_VOICE_CALL: - mContentType = CONTENT_TYPE_SPEECH; - break; - case AudioSystem.STREAM_SYSTEM_ENFORCED: - mFlags |= FLAG_AUDIBILITY_ENFORCED; - // intended fall through, attributes in common with STREAM_SYSTEM - case AudioSystem.STREAM_SYSTEM: - mContentType = CONTENT_TYPE_SONIFICATION; - break; - case AudioSystem.STREAM_RING: - mContentType = CONTENT_TYPE_SONIFICATION; - break; - case AudioSystem.STREAM_MUSIC: - mContentType = CONTENT_TYPE_MUSIC; - break; - case AudioSystem.STREAM_ALARM: - mContentType = CONTENT_TYPE_SONIFICATION; - break; - case AudioSystem.STREAM_NOTIFICATION: - mContentType = CONTENT_TYPE_SONIFICATION; - break; - case AudioSystem.STREAM_BLUETOOTH_SCO: - mContentType = CONTENT_TYPE_SPEECH; - mFlags |= FLAG_SCO; - break; - case AudioSystem.STREAM_DTMF: - mContentType = CONTENT_TYPE_SONIFICATION; - break; - case AudioSystem.STREAM_TTS: - mContentType = CONTENT_TYPE_SONIFICATION; - mFlags |= FLAG_BEACON; - break; - case AudioSystem.STREAM_ACCESSIBILITY: - mContentType = CONTENT_TYPE_SPEECH; - break; - case AudioSystem.STREAM_ASSISTANT: - mContentType = CONTENT_TYPE_SPEECH; - break; - default: - Log.e(TAG, "Invalid stream type " + streamType + " for AudioAttributes"); - } + switch (streamType) { + case AudioSystem.STREAM_VOICE_CALL: + mContentType = CONTENT_TYPE_SPEECH; + break; + case AudioSystem.STREAM_SYSTEM_ENFORCED: + mFlags |= FLAG_AUDIBILITY_ENFORCED; + // intended fall through, attributes in common with STREAM_SYSTEM + case AudioSystem.STREAM_SYSTEM: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioSystem.STREAM_RING: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioSystem.STREAM_ALARM: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioSystem.STREAM_NOTIFICATION: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioSystem.STREAM_BLUETOOTH_SCO: + mContentType = CONTENT_TYPE_SPEECH; + mFlags |= FLAG_SCO; + break; + case AudioSystem.STREAM_DTMF: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioSystem.STREAM_TTS: + mContentType = CONTENT_TYPE_SONIFICATION; + mFlags |= FLAG_BEACON; + break; + case AudioSystem.STREAM_ACCESSIBILITY: + mContentType = CONTENT_TYPE_SPEECH; + break; + case AudioSystem.STREAM_ASSISTANT: + mContentType = CONTENT_TYPE_SPEECH; + break; + case AudioSystem.STREAM_MUSIC: + // leaving as CONTENT_TYPE_UNKNOWN + break; + default: + Log.e(TAG, "Invalid stream type " + streamType + " for AudioAttributes"); } if (mUsage == USAGE_UNKNOWN) { mUsage = usageForStreamType(streamType); diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index f9ea7735681f4f837650b35022b251a6641bd647..49bd9d9940886df6178707d911c824ab2c745610 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -345,6 +345,8 @@ <string name="accessibility_wifi_three_bars">Wifi three bars.</string> <!-- Content description of the WIFI signal when it is full for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_wifi_signal_full">Wifi signal full.</string> + <!-- Content description of the WIFI signal when the WIFI is connected using the signal from a different device owned by the user. For accessibility (not shown on the screen) [CHAR LIMIT=NONE] --> + <string name="accessibility_wifi_other_device">Connected to your device.</string> <!-- Content description of the Wi-Fi security type. This message indicates this is an open Wi-Fi (no password needed) [CHAR LIMIT=NONE] --> <string name="accessibility_wifi_security_type_none">Open network</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java index ee65ef4e92b63a44d50edc225236a2b7efcdf819..ce466dfbf19c7ceaced4da4c47dabfbe7a570576 100644 --- a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java +++ b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java @@ -50,6 +50,7 @@ public class AccessibilityContentDescriptions { }; public static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi; + public static final int WIFI_OTHER_DEVICE_CONNECTION = R.string.accessibility_wifi_other_device; public static final int NO_CALLING = R.string.accessibility_no_calling; diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b5b873c8231fd45ad925d4e027334a3802563989..9bfc4be0f30d7e5fedf8f6f1d2bc18c00019352d 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -634,7 +634,7 @@ <!-- started from MediaProjectionManager --> <activity - android:name=".media.MediaProjectionPermissionActivity" + android:name=".mediaprojection.permission.MediaProjectionPermissionActivity" android:exported="true" android:theme="@style/Theme.SystemUI.MediaProjectionAlertDialog" android:finishOnCloseSystemDialogs="true" @@ -643,7 +643,7 @@ android:visibleToInstantApps="true"/> <activity - android:name=".media.MediaProjectionAppSelectorActivity" + android:name=".mediaprojection.appselector.MediaProjectionAppSelectorActivity" android:theme="@style/Theme.SystemUI.MediaProjectionAppSelector" android:finishOnCloseSystemDialogs="true" android:excludeFromRecents="true" diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp index 510fa1e1fa803020cb2688aba50bde3ab1999096..42d088f218a1d4511208e02e9509ba01f10e4c3f 100644 --- a/packages/SystemUI/compose/core/Android.bp +++ b/packages/SystemUI/compose/core/Android.bp @@ -34,7 +34,9 @@ android_library { "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", + "androidx.compose.material3_material3-window-size-class", "androidx.savedstate_savedstate", + "androidx.window_window", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt index 0cc259ab7015d7aaed4c8c44bab36ebbc46a7abe..a62c9840add189be7e23fd92bec29acf898d3df6 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt @@ -151,7 +151,7 @@ internal fun Modifier.element( element.lastAlpha = alpha } } - .testTag(key.name) + .testTag(key.testTag) } private fun shouldDrawElement( @@ -167,7 +167,8 @@ private fun shouldDrawElement( state.fromScene == state.toScene || !layoutImpl.isTransitionReady(state) || state.fromScene !in element.sceneValues || - state.toScene !in element.sceneValues + state.toScene !in element.sceneValues || + !isSharedElementEnabled(layoutImpl, state, element.key) ) { return true } @@ -191,6 +192,26 @@ private fun shouldDrawElement( } } +private fun isSharedElementEnabled( + layoutImpl: SceneTransitionLayoutImpl, + transition: TransitionState.Transition, + element: ElementKey, +): Boolean { + val spec = layoutImpl.transitions.transitionSpec(transition.fromScene, transition.toScene) + val sharedInFromScene = spec.transformations(element, transition.fromScene).shared + val sharedInToScene = spec.transformations(element, transition.toScene).shared + + // The sharedElement() transformation must either be null or be the same in both scenes. + if (sharedInFromScene != sharedInToScene) { + error( + "Different sharedElement() transformations matched $element (from=$sharedInFromScene " + + "to=$sharedInToScene)" + ) + } + + return sharedInFromScene?.enabled ?: true +} + /** * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied * throughout the current transition, if any. @@ -213,7 +234,7 @@ private fun Modifier.modifierTransformations( return layoutImpl.transitions .transitionSpec(fromScene, state.toScene) - .transformations(element.key) + .transformations(element.key, scene.key) .modifier .fold(this) { modifier, transformation -> with(transformation) { @@ -407,17 +428,20 @@ private inline fun <T> computeValue( // The element is shared: interpolate between the value in fromScene and the value in toScene. // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared // elements follow the finger direction. - if (fromValues != null && toValues != null) { + val isSharedElement = fromValues != null && toValues != null + if (isSharedElement && isSharedElementEnabled(layoutImpl, state, element.key)) { return lerp( - sceneValue(fromValues), - sceneValue(toValues), + sceneValue(fromValues!!), + sceneValue(toValues!!), transitionProgress, ) } val transformation = transformation( - layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key) + layoutImpl.transitions + .transitionSpec(fromScene, toScene) + .transformations(element.key, scene.key) ) // If there is no transformation explicitly associated to this element value, let's use // the value given by the system (like the current position and size given by the layout @@ -426,12 +450,21 @@ private inline fun <T> computeValue( // Get the transformed value, i.e. the target value at the beginning (for entering elements) or // end (for leaving elements) of the transition. + val sceneValues = + checkNotNull( + when { + isSharedElement && scene.key == fromScene -> fromValues + isSharedElement -> toValues + else -> fromValues ?: toValues + } + ) + val targetValue = transformation.transform( layoutImpl, scene, element, - fromValues ?: toValues!!, + sceneValues, state, idleValue, ) @@ -440,7 +473,7 @@ private inline fun <T> computeValue( val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress // Interpolate between the value at rest and the value before entering/after leaving. - val isEntering = fromValues == null + val isEntering = scene.key == toScene return if (isEntering) { lerp(targetValue, idleValue, rangeProgress) } else { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ElementMatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..98dbb67d7c6694a7b5e539eb4d61021425d4afd1 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ElementMatcher.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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.compose.animation.scene + +/** An interface to match one or more elements. */ +interface ElementMatcher { + /** Whether the element with key [key] in scene [scene] matches this matcher. */ + fun matches(key: ElementKey, scene: SceneKey): Boolean +} + +/** + * Returns an [ElementMatcher] that matches elements in [scene] also matching [this] + * [ElementMatcher]. + */ +fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher { + val delegate = this + val matcherScene = scene + return object : ElementMatcher { + override fun matches(key: ElementKey, scene: SceneKey): Boolean { + return scene == matcherScene && delegate.matches(key, scene) + } + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt index f7ebe2fc6d343c2f1f78f6451c497000fca618ed..b7acc48e286500c9c2b3e0dd4b8cd65bb58bc5bf 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt @@ -16,6 +16,8 @@ package com.android.compose.animation.scene +import androidx.annotation.VisibleForTesting + /** * A base class to create unique keys, associated to an [identity] that is used to check the * equality of two key instances. @@ -41,6 +43,7 @@ class SceneKey( name: String, identity: Any = Object(), ) : Key(name, identity) { + @VisibleForTesting val testTag: String = "scene:$name" /** The unique [ElementKey] identifying this scene's root element. */ val rootElementKey = ElementKey(name, identity) @@ -61,7 +64,9 @@ class ElementKey( */ val isBackground: Boolean = false, ) : Key(name, identity), ElementMatcher { - override fun matches(key: ElementKey): Boolean { + @VisibleForTesting val testTag: String = "element:$name" + + override fun matches(key: ElementKey, scene: SceneKey): Boolean { return key == this } @@ -73,7 +78,9 @@ class ElementKey( /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */ fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher { return object : ElementMatcher { - override fun matches(key: ElementKey): Boolean = predicate(key.identity) + override fun matches(key: ElementKey, scene: SceneKey): Boolean { + return predicate(key.identity) + } } } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt index b44c8efc7ee21fc07b12aa2e1885ee3fec6feeb6..3985233bd19779e49763b6f4da71028443122e04 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex @@ -45,7 +46,9 @@ internal class Scene( @Composable fun Content(modifier: Modifier = Modifier) { - Box(modifier.zIndex(zIndex).onPlaced { size = it.size }) { scope.content() } + Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) { + scope.content() + } } override fun toString(): String { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt index f4e39023edfe9c9404302e5f14558b7980c0605c..75dcb2e44c135befcff683741a796cdde1491ad0 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import androidx.annotation.VisibleForTesting import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.snap import androidx.compose.ui.geometry.Offset @@ -28,6 +29,7 @@ import com.android.compose.animation.scene.transformation.ModifierTransformation import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize +import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.animation.scene.transformation.Transformation import com.android.compose.animation.scene.transformation.Translate import com.android.compose.ui.util.fastForEach @@ -35,11 +37,12 @@ import com.android.compose.ui.util.fastMap /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions( - private val transitionSpecs: List<TransitionSpec>, + @get:VisibleForTesting val transitionSpecs: List<TransitionSpec>, ) { private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>() - internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec { + @VisibleForTesting + fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec { return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) } } @@ -97,7 +100,8 @@ data class TransitionSpec( val transformations: List<Transformation>, val spec: AnimationSpec<Float>, ) { - private val cache = mutableMapOf<ElementKey, ElementTransformations>() + // TODO(b/302300957): Make sure this cache does not infinitely grow. + private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>() internal fun reverse(): TransitionSpec { return copy( @@ -107,12 +111,18 @@ data class TransitionSpec( ) } - internal fun transformations(element: ElementKey): ElementTransformations { - return cache.getOrPut(element) { computeTransformations(element) } + internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations { + return cache + .getOrPut(element) { mutableMapOf() } + .getOrPut(scene) { computeTransformations(element, scene) } } /** Filter [transformations] to compute the [ElementTransformations] of [element]. */ - private fun computeTransformations(element: ElementKey): ElementTransformations { + private fun computeTransformations( + element: ElementKey, + scene: SceneKey, + ): ElementTransformations { + var shared: SharedElementTransformation? = null val modifier = mutableListOf<ModifierTransformation>() var offset: PropertyTransformation<Offset>? = null var size: PropertyTransformation<IntSize>? = null @@ -126,16 +136,16 @@ data class TransitionSpec( is Translate, is EdgeTranslate, is AnchoredTranslate -> { - throwIfNotNull(offset, element, property = "offset") + throwIfNotNull(offset, element, name = "offset") offset = root as PropertyTransformation<Offset> } is ScaleSize, is AnchoredSize -> { - throwIfNotNull(size, element, property = "size") + throwIfNotNull(size, element, name = "size") size = root as PropertyTransformation<IntSize> } is Fade -> { - throwIfNotNull(alpha, element, property = "alpha") + throwIfNotNull(alpha, element, name = "alpha") alpha = root as PropertyTransformation<Float> } is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate) @@ -143,32 +153,37 @@ data class TransitionSpec( } transformations.fastForEach { transformation -> - if (!transformation.matcher.matches(element)) { + if (!transformation.matcher.matches(element, scene)) { return@fastForEach } when (transformation) { + is SharedElementTransformation -> { + throwIfNotNull(shared, element, name = "shared") + shared = transformation + } is ModifierTransformation -> modifier.add(transformation) is PropertyTransformation<*> -> onPropertyTransformation(transformation) } } - return ElementTransformations(modifier, offset, size, alpha) + return ElementTransformations(shared, modifier, offset, size, alpha) } private fun throwIfNotNull( - previous: PropertyTransformation<*>?, + previous: Transformation?, element: ElementKey, - property: String, + name: String, ) { if (previous != null) { - error("$element has multiple transformations for its $property property") + error("$element has multiple $name transformations") } } } /** The transformations of an element during a transition. */ internal class ElementTransformations( + val shared: SharedElementTransformation?, val modifier: List<ModifierTransformation>, val offset: PropertyTransformation<Offset>?, val size: PropertyTransformation<IntSize>?, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt index fb12b90d7d3e928716581697ec7f248acb57ecd7..49669775fedd7d4d2ae7088055ef88b4d78994fb 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -115,6 +115,14 @@ interface TransitionBuilder : PropertyTransformationBuilder { builder: PropertyTransformationBuilder.() -> Unit, ) + /** + * Configure the shared transition when [matcher] is shared between two scenes. + * + * @param enabled whether the matched element(s) should actually be shared in this transition. + * Defaults to true. + */ + fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) + /** * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and * using the given [shape]. @@ -127,6 +135,13 @@ interface TransitionBuilder : PropertyTransformationBuilder { * the result. */ fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape) + + /** + * Adds the transformations in [builder] but in reversed order. This allows you to partially + * reuse the definition of the transition from scene `Foo` to scene `Bar` inside the definition + * of the transition from scene `Bar` to scene `Foo`. + */ + fun reversed(builder: TransitionBuilder.() -> Unit) } @TransitionDsl @@ -179,12 +194,6 @@ interface PropertyTransformationBuilder { fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) } -/** An interface to match one or more elements. */ -interface ElementMatcher { - /** Whether the element with key [key] matches this matcher. */ - fun matches(key: ElementKey): Boolean -} - /** The edge of a [SceneTransitionLayout]. */ enum class Edge { Left, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 48d5638e8b4ef86b2d967158dedc3c723210dec1..f1c27178391c67bd0eef2ee44bcef3e4952afd84 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -31,6 +31,7 @@ import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.PunchHole import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize +import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.animation.scene.transformation.Transformation import com.android.compose.animation.scene.transformation.TransformationRange import com.android.compose.animation.scene.transformation.Translate @@ -80,6 +81,7 @@ internal class TransitionBuilderImpl : TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) private var range: TransformationRange? = null + private var reversed = false private val durationMillis: Int by lazy { val spec = spec if (spec !is DurationBasedAnimationSpec) { @@ -93,6 +95,12 @@ internal class TransitionBuilderImpl : TransitionBuilder { transformations.add(PunchHole(matcher, bounds, shape)) } + override fun reversed(builder: TransitionBuilder.() -> Unit) { + reversed = true + builder() + reversed = false + } + override fun fractionRange( start: Float?, end: Float?, @@ -103,6 +111,10 @@ internal class TransitionBuilderImpl : TransitionBuilder { range = null } + override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { + transformations.add(SharedElementTransformation(matcher, enabled)) + } + override fun timestampRange( startMillis: Int?, endMillis: Int?, @@ -122,11 +134,20 @@ internal class TransitionBuilderImpl : TransitionBuilder { } private fun transformation(transformation: PropertyTransformation<*>) { - if (range != null) { - transformations.add(RangedPropertyTransformation(transformation, range!!)) - } else { - transformations.add(transformation) - } + val transformation = + if (range != null) { + RangedPropertyTransformation(transformation, range!!) + } else { + transformation + } + + transformations.add( + if (reversed) { + transformation.reverse() + } else { + transformation + } + ) } override fun fade(matcher: ElementMatcher) { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt index ce6749da271103e45d9a254ecb6cb5bbf237d9cf..a65025423aee55fd0fe6a29565f05b388dc8ea41 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -30,6 +30,14 @@ sealed interface Transformation { */ val matcher: ElementMatcher + /** + * The range during which the transformation is applied. If it is `null`, then the + * transformation will be applied throughout the whole scene transition. + */ + // TODO(b/240432457): Move this back to PropertyTransformation. + val range: TransformationRange? + get() = null + /* * Reverse this transformation. This is called when we use Transition(from = A, to = B) when * animating from B to A and there is no Transition(from = B, to = A) defined. @@ -37,6 +45,11 @@ sealed interface Transformation { fun reverse(): Transformation = this } +internal class SharedElementTransformation( + override val matcher: ElementMatcher, + internal val enabled: Boolean, +) : Transformation + /** A transformation that is applied on the element during the whole transition. */ internal interface ModifierTransformation : Transformation { /** Apply the transformation to [element]. */ @@ -52,13 +65,6 @@ internal interface ModifierTransformation : Transformation { /** A transformation that changes the value of an element property, like its size or offset. */ internal sealed interface PropertyTransformation<T> : Transformation { - /** - * The range during which the transformation is applied. If it is `null`, then the - * transformation will be applied throughout the whole scene transition. - */ - val range: TransformationRange? - get() = null - /** * Transform [value], i.e. the value of the transformed property without this transformation. */ @@ -92,8 +98,7 @@ internal class RangedPropertyTransformation<T>( } /** The progress-based range of a [PropertyTransformation]. */ -data class TransformationRange -private constructor( +data class TransformationRange( val start: Float, val end: Float, ) { @@ -133,6 +138,6 @@ private constructor( } companion object { - private const val BoundUnspecified = Float.MIN_VALUE + const val BoundUnspecified = Float.MIN_VALUE } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt index b4e90d63c6b8f2cec06aee72a890fd2203b92ae3..06618704e085089a7d54c24213222b849f4649a8 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt @@ -29,6 +29,8 @@ import com.android.compose.theme.typography.TypefaceNames import com.android.compose.theme.typography.TypefaceTokens import com.android.compose.theme.typography.TypographyTokens import com.android.compose.theme.typography.platformTypography +import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.compose.windowsizeclass.calculateWindowSizeClass /** The Material 3 theme that should wrap all Platform Composables. */ @Composable @@ -51,10 +53,12 @@ fun PlatformTheme( remember(typefaceNames) { platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames)))) } + val windowSizeClass = calculateWindowSizeClass() MaterialTheme(colorScheme, typography = typography) { CompositionLocalProvider( LocalAndroidColorScheme provides androidColorScheme, + LocalWindowSizeClass provides windowSizeClass, ) { content() } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/windowsizeclass/WindowSizeClass.kt b/packages/SystemUI/compose/core/src/com/android/compose/windowsizeclass/WindowSizeClass.kt new file mode 100644 index 0000000000000000000000000000000000000000..4674d6e5f25a720335ab74d1f817de88410a87df --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/windowsizeclass/WindowSizeClass.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 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.compose.windowsizeclass + +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.window.layout.WindowMetricsCalculator + +val LocalWindowSizeClass = + staticCompositionLocalOf<WindowSizeClass> { + throw IllegalStateException( + "No WindowSizeClass configured. Make sure to use LocalWindowSizeClass in a Composable" + + " surrounded by a PlatformTheme {}." + ) + } + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun calculateWindowSizeClass(): WindowSizeClass { + // Observe view configuration changes and recalculate the size class on each change. + LocalConfiguration.current + val density = LocalDensity.current + val context = LocalContext.current + val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) + val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() } + return WindowSizeClass.calculateFromSize(size) +} diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 8bd654585f29bf78a9a82bf83a0704245d1e9903..328866ea76cab165266d153d7d12aa6846312c72 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -224,7 +224,7 @@ class SceneTransitionLayoutTest { // In scene A, the shared element SharedFoo() is at the top end of the layout and has a size // of 50.dp. - var sharedFoo = rule.onNodeWithTag(TestElements.Foo.name, useUnmergedTree = true) + var sharedFoo = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) sharedFoo.assertWidthIsEqualTo(50.dp) sharedFoo.assertHeightIsEqualTo(50.dp) sharedFoo.assertPositionInRootIsEqualTo( @@ -250,7 +250,7 @@ class SceneTransitionLayoutTest { // We need to use onAllNodesWithTag().onFirst() here given that shared elements are // composed and laid out in both scenes (but drawn only in one). - sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst() + sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.testTag).onFirst() // In scene B, foo is at the top start (x = 0, y = 0) of the layout and has a size of // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we @@ -284,7 +284,7 @@ class SceneTransitionLayoutTest { val expectedLeft = 0.dp val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress - sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst() + sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.testTag).onFirst() assertThat((layoutState.transitionState as TransitionState.Transition).progress) .isEqualTo(interpolatedProgress) sharedFoo.assertWidthIsEqualTo(expectedSize) diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt index 275149a05abfad88d258f72f919c1692473e40b5..268057fd2f2c1132232ee940a7059b81741a709c 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt @@ -23,7 +23,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.hasParent +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag @DslMarker annotation class TransitionTestDsl @@ -59,8 +63,21 @@ interface TransitionTestBuilder { @TransitionTestDsl interface TransitionTestAssertionScope { - /** Assert on [element]. */ - fun onElement(element: ElementKey): SemanticsNodeInteraction + /** + * Assert on [element]. + * + * Note that presence/value assertions on the returned [SemanticsNodeInteraction] will fail if 0 + * or more than 1 elements matched [element]. If you need to assert on a shared element that + * will be present multiple times in the layout during transitions, either specify the [scene] + * in which you are matching or use [onSharedElement] instead. + */ + fun onElement(element: ElementKey, scene: SceneKey? = null): SemanticsNodeInteraction + + /** + * Assert on a shared [element]. This will throw if [element] is not shared and present only in + * one scene during a transition. + */ + fun onSharedElement(element: ElementKey): SemanticsNodeInteractionCollection } /** @@ -73,20 +90,22 @@ fun ComposeContentTestRule.testTransition( toSceneContent: @Composable SceneScope.() -> Unit, transition: TransitionBuilder.() -> Unit, layoutModifier: Modifier = Modifier, + fromScene: SceneKey = TestScenes.SceneA, + toScene: SceneKey = TestScenes.SceneB, builder: TransitionTestBuilder.() -> Unit, ) { testTransition( - from = TestScenes.SceneA, - to = TestScenes.SceneB, + from = fromScene, + to = toScene, transitionLayout = { currentScene, onChangeScene -> SceneTransitionLayout( currentScene, onChangeScene, - transitions { from(TestScenes.SceneA, to = TestScenes.SceneB, transition) }, + transitions { from(fromScene, to = toScene, transition) }, layoutModifier.fillMaxSize(), ) { - scene(TestScenes.SceneA, content = fromSceneContent) - scene(TestScenes.SceneB, content = toSceneContent) + scene(fromScene, content = fromSceneContent) + scene(toScene, content = toSceneContent) } }, builder, @@ -111,8 +130,24 @@ fun ComposeContentTestRule.testTransition( val test = transitionTest(builder) val assertionScope = object : TransitionTestAssertionScope { - override fun onElement(element: ElementKey): SemanticsNodeInteraction { - return this@testTransition.onNodeWithTag(element.name) + override fun onElement( + element: ElementKey, + scene: SceneKey? + ): SemanticsNodeInteraction { + return if (scene == null) { + onNodeWithTag(element.testTag) + } else { + onNode(hasTestTag(element.testTag) and hasParent(hasTestTag(scene.testTag))) + } + } + + override fun onSharedElement(element: ElementKey): SemanticsNodeInteractionCollection { + val interaction = onAllNodesWithTag(element.testTag) + val matches = interaction.fetchSemanticsNodes(atLeastOneRootRequired = false).size + if (matches < 2) { + error("Element $element is not shared ($matches matches)") + } + return interaction } } diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa94b25028a23e7b24e59930065fc7bfe93e654f --- /dev/null +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 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.compose.animation.scene + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.tween +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.transformation.Transformation +import com.android.compose.animation.scene.transformation.TransformationRange +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TransitionDslTest { + @Test + fun emptyTransitions() { + val transitions = transitions {} + assertThat(transitions.transitionSpecs).isEmpty() + } + + @Test + fun manyTransitions() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) + from(TestScenes.SceneB, to = TestScenes.SceneC) + from(TestScenes.SceneC, to = TestScenes.SceneA) + } + assertThat(transitions.transitionSpecs).hasSize(3) + } + + @Test + fun toFromBuilders() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) + from(TestScenes.SceneB) + to(TestScenes.SceneC) + } + + assertThat(transitions.transitionSpecs) + .comparingElementsUsing( + Correspondence.transforming<TransitionSpec, Pair<SceneKey?, SceneKey?>>( + { it?.from to it?.to }, + "has (from, to) equal to" + ) + ) + .containsExactly( + TestScenes.SceneA to TestScenes.SceneB, + TestScenes.SceneB to null, + null to TestScenes.SceneC, + ) + } + + @Test + fun defaultTransitionSpec() { + val transitions = transitions { from(TestScenes.SceneA, to = TestScenes.SceneB) } + val transition = transitions.transitionSpecs.single() + assertThat(transition.spec).isInstanceOf(SpringSpec::class.java) + } + + @Test + fun customTransitionSpec() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { spec = tween(durationMillis = 42) } + } + val transition = transitions.transitionSpecs.single() + assertThat(transition.spec).isInstanceOf(TweenSpec::class.java) + assertThat((transition.spec as TweenSpec).durationMillis).isEqualTo(42) + } + + @Test + fun defaultRange() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) } + } + + val transition = transitions.transitionSpecs.single() + assertThat(transition.transformations.size).isEqualTo(1) + assertThat(transition.transformations.single().range).isEqualTo(null) + } + + @Test + fun fractionRange() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { + fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } + fractionRange(start = 0.2f) { fade(TestElements.Foo) } + fractionRange(end = 0.9f) { fade(TestElements.Foo) } + } + } + + val transition = transitions.transitionSpecs.single() + assertThat(transition.transformations) + .comparingElementsUsing(TRANSFORMATION_RANGE) + .containsExactly( + TransformationRange(start = 0.1f, end = 0.8f), + TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified), + TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f), + ) + } + + @Test + fun timestampRange() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { + spec = tween(500) + + timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } + timestampRange(startMillis = 200) { fade(TestElements.Foo) } + timestampRange(endMillis = 400) { fade(TestElements.Foo) } + } + } + + val transition = transitions.transitionSpecs.single() + assertThat(transition.transformations) + .comparingElementsUsing(TRANSFORMATION_RANGE) + .containsExactly( + TransformationRange(start = 100 / 500f, end = 300 / 500f), + TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified), + TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f), + ) + } + + @Test + fun reversed() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { + spec = tween(500) + reversed { + fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } + timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } + } + } + } + + val transition = transitions.transitionSpecs.single() + assertThat(transition.transformations) + .comparingElementsUsing(TRANSFORMATION_RANGE) + .containsExactly( + TransformationRange(start = 1f - 0.8f, end = 1f - 0.1f), + TransformationRange(start = 1f - 300 / 500f, end = 1f - 100 / 500f), + ) + } + + @Test + fun defaultReversed() { + val transitions = transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { + spec = tween(500) + fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } + timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } + } + } + + // Fetch the transition from B to A, which will automatically reverse the transition from A + // to B we defined. + val transition = + transitions.transitionSpec(from = TestScenes.SceneB, to = TestScenes.SceneA) + assertThat(transition.transformations) + .comparingElementsUsing(TRANSFORMATION_RANGE) + .containsExactly( + TransformationRange(start = 1f - 0.8f, end = 1f - 0.1f), + TransformationRange(start = 1f - 300 / 500f, end = 1f - 100 / 500f), + ) + } + + companion object { + private val TRANSFORMATION_RANGE = + Correspondence.transforming<Transformation, TransformationRange?>( + { it?.range }, + "has range equal to" + ) + } +} diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2af3638602720ca5fc5e12d3ff4d3e728a847776 --- /dev/null +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2023 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.compose.animation.scene.transformation + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertPositionInRootIsEqualTo +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.TestElements +import com.android.compose.animation.scene.TestScenes +import com.android.compose.animation.scene.inScene +import com.android.compose.animation.scene.testTransition +import com.android.compose.modifiers.size +import com.android.compose.test.assertSizeIsEqualTo +import com.android.compose.test.onEach +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SharedElementTest { + @get:Rule val rule = createComposeRule() + + @Test + fun testSharedElement() { + rule.testTransition( + fromSceneContent = { + // Foo is at (10, 50) with a size of (20, 80). + Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo).size(20.dp, 80.dp)) + }, + toSceneContent = { + // Foo is at (50, 70) with a size of (10, 40). + Box(Modifier.offset(50.dp, 70.dp).element(TestElements.Foo).size(10.dp, 40.dp)) + }, + transition = { + spec = tween(16 * 4, easing = LinearEasing) + // Elements should be shared by default. + } + ) { + before { + onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) + onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp) + } + at(0) { + onSharedElement(TestElements.Foo).onEach { + assertPositionInRootIsEqualTo(10.dp, 50.dp) + assertSizeIsEqualTo(20.dp, 80.dp) + } + } + at(16) { + onSharedElement(TestElements.Foo).onEach { + assertPositionInRootIsEqualTo(20.dp, 55.dp) + assertSizeIsEqualTo(17.5.dp, 70.dp) + } + } + at(32) { + onSharedElement(TestElements.Foo).onEach { + assertPositionInRootIsEqualTo(30.dp, 60.dp) + assertSizeIsEqualTo(15.dp, 60.dp) + } + } + at(48) { + onSharedElement(TestElements.Foo).onEach { + assertPositionInRootIsEqualTo(40.dp, 65.dp) + assertSizeIsEqualTo(12.5.dp, 50.dp) + } + } + after { + onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp) + onElement(TestElements.Foo).assertSizeIsEqualTo(10.dp, 40.dp) + } + } + } + + @Test + fun testSharedElementDisabled() { + rule.testTransition( + fromScene = TestScenes.SceneA, + toScene = TestScenes.SceneB, + // The full layout is 100x100. + layoutModifier = Modifier.size(100.dp), + fromSceneContent = { + Box(Modifier.fillMaxSize()) { + // Foo is at (10, 50). + Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo)) + } + }, + toSceneContent = { + Box(Modifier.fillMaxSize()) { + // Foo is at (50, 60). + Box(Modifier.offset(50.dp, 60.dp).element(TestElements.Foo)) + } + }, + transition = { + spec = tween(16 * 4, easing = LinearEasing) + + // Disable the shared element animation. + sharedElement(TestElements.Foo, enabled = false) + + // In SceneA, Foo leaves to the left edge. + translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left) + + // In SceneB, Foo comes from the bottom edge. + translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom) + }, + ) { + before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) } + at(0) { + onElement(TestElements.Foo, scene = TestScenes.SceneA) + .assertPositionInRootIsEqualTo(10.dp, 50.dp) + onElement(TestElements.Foo, scene = TestScenes.SceneB) + .assertPositionInRootIsEqualTo(50.dp, 100.dp) + } + at(16) { + onElement(TestElements.Foo, scene = TestScenes.SceneA) + .assertPositionInRootIsEqualTo(7.5.dp, 50.dp) + onElement(TestElements.Foo, scene = TestScenes.SceneB) + .assertPositionInRootIsEqualTo(50.dp, 90.dp) + } + at(32) { + onElement(TestElements.Foo, scene = TestScenes.SceneA) + .assertPositionInRootIsEqualTo(5.dp, 50.dp) + onElement(TestElements.Foo, scene = TestScenes.SceneB) + .assertPositionInRootIsEqualTo(50.dp, 80.dp) + } + at(48) { + onElement(TestElements.Foo, scene = TestScenes.SceneA) + .assertPositionInRootIsEqualTo(2.5.dp, 50.dp) + onElement(TestElements.Foo, scene = TestScenes.SceneB) + .assertPositionInRootIsEqualTo(50.dp, 70.dp) + } + after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 60.dp) } + } + } +} diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/Selectors.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/Selectors.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6f64bfe49741da80b7fdc82668b49e9268aebbe --- /dev/null +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/Selectors.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 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.compose.test + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection + +/** Assert [assert] on each element of [this] [SemanticsNodeInteractionCollection]. */ +fun SemanticsNodeInteractionCollection.onEach(assert: SemanticsNodeInteraction.() -> Unit) { + for (i in 0 until this.fetchSemanticsNodes().size) { + get(i).assert() + } +} diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml index 569dd4cf92528837645cebc4f37a3a837ddf963b..a51c55ee965f47b6e6a138f3a05a395c64c50239 100644 --- a/packages/SystemUI/res/layout/connected_display_dialog.xml +++ b/packages/SystemUI/res/layout/connected_display_dialog.xml @@ -52,7 +52,7 @@ style="@style/Widget.Dialog.Button.BorderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/cancel" /> + android:text="@string/dismiss_dialog" /> <Space android:layout_width="0dp" @@ -64,6 +64,6 @@ style="@style/Widget.Dialog.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/enable_display" /> + android:text="@string/mirror_display" /> </LinearLayout> </LinearLayout> \ No newline at end of file diff --git a/packages/SystemUI/res/values-land/config.xml b/packages/SystemUI/res/values-land/config.xml index 85fb3ac577bcc0700615528e8febb3df0eb26389..587caaf3ecf3e301c0c81c03709a6b642c756dba 100644 --- a/packages/SystemUI/res/values-land/config.xml +++ b/packages/SystemUI/res/values-land/config.xml @@ -44,6 +44,6 @@ <!-- Whether to force split shade. For now, this value has effect only when flag lockscreen.enable_landscape is enabled. - TODO (b/293290851) - change this comment/resource when flag is enabled --> + TODO (b/293252410) - change this comment/resource when flag is enabled --> <bool name="force_config_use_split_notification_shade">true</bool> </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/config.xml b/packages/SystemUI/res/values-sw600dp-land/config.xml index e63229aea70b353589d9cf1385a5d5332b328582..fc6d20e11d3b34b1907fdc56a8a3f94a0896e4f7 100644 --- a/packages/SystemUI/res/values-sw600dp-land/config.xml +++ b/packages/SystemUI/res/values-sw600dp-land/config.xml @@ -38,6 +38,6 @@ <!-- Whether to force split shade. For now, this value has effect only when flag lockscreen.enable_landscape is enabled. - TODO (b/293290851) - change this comment/resource when flag is enabled --> + TODO (b/293252410) - change this comment/resource when flag is enabled --> <bool name="force_config_use_split_notification_shade">false</bool> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index b6bca65b8174437b3a66d3bbc7a31b9e6466a1ef..3bdc0af7f7af20a386eafb96360b8fdc549ce0d9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -604,7 +604,7 @@ <!-- Whether to force split shade. For now, this value has effect only when flag lockscreen.enable_landscape is enabled. - TODO (b/293290851) - change this comment/resource when flag is enabled --> + TODO (b/293252410) - change this comment/resource when flag is enabled --> <bool name="force_config_use_split_notification_shade">false</bool> <!-- Whether we use large screen shade header which takes only one row compared to QS header --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 5860806c6aec36a257d2a10a4cadaa7f671220f5..a2637d5e55c3864d377e2c08845302e5aae55bda 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3214,9 +3214,10 @@ <!--- Title of the dialog appearing when an external display is connected, asking whether to start mirroring [CHAR LIMIT=NONE]--> <string name="connected_display_dialog_start_mirroring">Mirror to external display?</string> - <!--- Label of the "enable display" button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]--> - <string name="enable_display">Enable display</string> + <string name="mirror_display">Mirror display</string> + <!--- Label of the dismiss button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]--> + <string name="dismiss_dialog">Dismiss</string> <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] --> <string name="privacy_dialog_title">Microphone & Camera</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index ba8e427525869618b2a49961f968658212fba589..f6a0563ebf94f8072eda5fc624eef248dad62c74 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -184,6 +184,10 @@ public class KeyguardClockSwitch extends RelativeLayout { } } + public boolean getSplitShadeCentered() { + return mSplitShadeCentered; + } + @Override protected void onFinishInflate() { super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 29414ea7f4c0ae519788964b283d51fb2946ac5a..5646abefcef1be27fba73dbfb87b8b491258a7ce 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -234,6 +234,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } } + public KeyguardClockSwitch getView() { + return mView; + } + private void hideSliceViewAndNotificationIconContainer() { View ksv = mView.findViewById(R.id.keyguard_slice_view); ksv.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index d8486029a9030dfe3a1eb5c5532d9717a46f3a13..f9cc03eea288f5a5c239a64014794ebae472d501 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -18,6 +18,7 @@ package com.android.keyguard; import static java.util.Collections.emptySet; +import android.animation.LayoutTransition; import android.content.Context; import android.graphics.Canvas; import android.os.Build; @@ -78,6 +79,14 @@ public class KeyguardStatusView extends GridLayout { mKeyguardSlice = findViewById(R.id.keyguard_slice_view); mMediaHostContainer = findViewById(R.id.status_view_media_container); + if (mMediaHostContainer != null) { + LayoutTransition mediaLayoutTransition = new LayoutTransition(); + ((ViewGroup) mMediaHostContainer).setLayoutTransition(mediaLayoutTransition); + mediaLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); + mediaLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); + mediaLayoutTransition.disableTransitionType(LayoutTransition.APPEARING); + mediaLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING); + } updateDark(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 8d0d299e56c2ab7bb8e0312c561cb6cf2a77b3c9..931ba6d97c5865a16eac1cc4b866b570f3d83433 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -23,6 +23,7 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CL import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; +import android.animation.LayoutTransition; import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.res.Configuration; @@ -101,6 +102,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV private final Rect mClipBounds = new Rect(); private final KeyguardInteractor mKeyguardInteractor; + private Boolean mSplitShadeEnabled = false; private Boolean mStatusViewCentered = true; private DumpManager mDumpManager; @@ -150,6 +152,48 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Override public void onInit() { mKeyguardClockSwitchController.init(); + final View mediaHostContainer = mView.findViewById(R.id.status_view_media_container); + if (mediaHostContainer != null) { + mKeyguardClockSwitchController.getView().addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (!mSplitShadeEnabled + || mKeyguardClockSwitchController.getView().getSplitShadeCentered() + // Note: isKeyguardVisible() returns false after Launcher -> AOD. + || !mKeyguardUpdateMonitor.isKeyguardVisible()) { + return; + } + + int oldHeight = oldBottom - oldTop; + if (v.getHeight() == oldHeight) return; + + if (mediaHostContainer.getVisibility() != View.VISIBLE + // If the media is appearing, also don't do the transition. + || mediaHostContainer.getHeight() == 0) { + return; + } + + final LayoutTransition mediaLayoutTransition = + ((ViewGroup) mediaHostContainer).getLayoutTransition(); + if (mediaLayoutTransition == null) return; + + mediaLayoutTransition.enableTransitionType(LayoutTransition.CHANGING); + }); + + mediaHostContainer.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + final LayoutTransition mediaLayoutTransition = + ((ViewGroup) mediaHostContainer).getLayoutTransition(); + if (mediaLayoutTransition == null) return; + if (!mediaLayoutTransition.isTransitionTypeEnabled( + LayoutTransition.CHANGING)) { + return; + } + // Note: when this is called, the LayoutTransition is already been set up. + // Disables the LayoutTransition until it's explicitly enabled again. + mediaLayoutTransition.disableTransitionType(LayoutTransition.CHANGING); + } + ); + } mDumpManager.registerDumpable(getInstanceName(), this); if (mFeatureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)) { @@ -385,6 +429,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV */ public void setSplitShadeEnabled(boolean enabled) { mKeyguardClockSwitchController.setSplitShadeEnabled(enabled); + mSplitShadeEnabled = enabled; } /** diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 165c4bb018d373a7ea0d6939844f7d09994e8361..a81069a1f7dbd85cc3064e49573feb1aab08ebc2 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -424,11 +424,11 @@ public class LockIconViewController implements Dumpable { private void updateConfiguration() { WindowManager windowManager = mContext.getSystemService(WindowManager.class); Rect bounds = windowManager.getCurrentWindowMetrics().getBounds(); - WindowInsets insets = windowManager.getCurrentWindowMetrics().getWindowInsets(); mWidthPixels = bounds.right; if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { // Assumed to be initially neglected as there are no left or right insets in portrait // However, on landscape, these insets need to included when calculating the midpoint + WindowInsets insets = windowManager.getCurrentWindowMetrics().getWindowInsets(); mWidthPixels -= insets.getSystemWindowInsetLeft() + insets.getSystemWindowInsetRight(); } mHeightPixels = bounds.bottom; diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3b70555ad32a68757eed724f9070195d741abe6c..871257e6be79a69afa2074e85b9490efd3a088e2 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -303,6 +303,11 @@ object Flags { @JvmField val MIGRATE_CLOCKS_TO_BLUEPRINT = unreleasedFlag("migrate_clocks_to_blueprint") + /** Migrate KeyguardRootView to use composables. */ + // TODO(b/301969856): Tracking Bug. + @JvmField val KEYGUARD_ROOT_VIEW_USE_COMPOSE = + unreleasedFlag("keyguard_root_view_use_compose") + /** Enables preview loading animation in the wallpaper picker. */ // TODO(b/274443705): Tracking Bug @JvmField @@ -771,6 +776,10 @@ object Flags { // TODO(b/302087895): Tracking Bug @JvmField val CALL_LAYOUT_ASYNC_SET_DATA = unreleasedFlag("call_layout_async_set_data") + // TODO(b/302144438): Tracking Bug + @JvmField val DECOUPLE_REMOTE_INPUT_DELEGATE_AND_CALLBACK_UPDATE = + unreleasedFlag("decouple_remote_input_delegate_and_callback_update") + // 2900 - CentralSurfaces-related flags // TODO(b/285174336): Tracking Bug diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index fb02c7d68a9f7d880dcb34a2422880c3c51ab511..1761ca86f5883d813e65192d4da2778f2c5a816f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -26,7 +26,6 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -40,7 +39,9 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel +import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -69,6 +70,7 @@ constructor( private val context: Context, private val keyguardIndicationController: KeyguardIndicationController, private val lockIconViewController: LockIconViewController, + private val shadeInteractor: ShadeInteractor, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -135,6 +137,7 @@ constructor( occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, keyguardStateController, + shadeInteractor ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index f564d0073e4290a3769d17baf4771caaa9ee9c45..053727ace76d0ad956f14e8d6e80441f1e690e89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -32,6 +32,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.ViewPriority @@ -55,6 +56,7 @@ object KeyguardRootViewBinder { occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, chipbarCoordinator: ChipbarCoordinator, keyguardStateController: KeyguardStateController, + shadeInteractor: ShadeInteractor, ): DisposableHandle { val disposableHandle = view.repeatWhenAttached { @@ -88,6 +90,17 @@ object KeyguardRootViewBinder { } } } + + launch { + shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded -> + view.visibility = + if (isFullyAnyExpanded) { + View.INVISIBLE + } else { + View.VISIBLE + } + } + } } repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 2ad74fbc6674d5e83fc96fecd7aee579e0447b76..864e345e9c1e71b46df6842f5d5ad07bfd3960b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -62,6 +62,7 @@ import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessage import com.android.systemui.monet.ColorScheme import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.FalsingManager +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants @@ -109,6 +110,7 @@ constructor( private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, private val keyguardStateController: KeyguardStateController, + private val shadeInteractor: ShadeInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) @@ -317,6 +319,7 @@ constructor( occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, keyguardStateController, + shadeInteractor, ) ) rootView.addView( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt index 9409036586e808f50a9fd1fe05ff609df3e9e22f..f4bc7137b50cfb38166f8ec7cf12063ab92dea4d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt @@ -29,11 +29,11 @@ import androidx.constraintlayout.widget.ConstraintSet import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController -import com.android.systemui.res.R import com.android.systemui.biometrics.AuthController import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import javax.inject.Inject @@ -73,11 +73,11 @@ constructor( val mBottomPaddingPx = context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) val bounds = windowManager.currentWindowMetrics.bounds - val insets = windowManager.currentWindowMetrics.windowInsets var widthPixels = bounds.right.toFloat() if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { // Assumed to be initially neglected as there are no left or right insets in portrait. // However, on landscape, these insets need to included when calculating the midpoint. + val insets = windowManager.currentWindowMetrics.windowInsets widthPixels -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat() } val heightPixels = bounds.bottom.toFloat() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index ae3c912d6d1bc21388ce315a45384acee29e7223..ed6d41e5a75bc926f613dfe7ea8c810b9a53131e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -101,7 +101,8 @@ constructor( panelEventsEvents: ShadeStateEvents, private val secureSettings: SecureSettings, @Main private val handler: Handler, - private val splitShadeStateController: SplitShadeStateController + private val splitShadeStateController: SplitShadeStateController, + private val logger: MediaViewLogger, ) { /** Track the media player setting status on lock screen. */ @@ -1057,6 +1058,7 @@ constructor( // that and directly set the mediaFrame's bounds within the premeasured host. targetHost.addView(mediaFrame) } + logger.logMediaHostAttachment(currentAttachmentLocation) if (isCrossFadeAnimatorRunning) { // When cross-fading with an animation, we only notify the media carousel of the // location change, once the view is reattached to the new place and not diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt index 8f1595d7d7a2fb49b7cffa4d24daae485b972ca0..3ff2315956ad694f9d99d687bbdcc2f70f4d507f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt @@ -52,4 +52,8 @@ class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogB { "location ($str1): $int1 -> $int2" } ) } + + fun logMediaHostAttachment(host: Int) { + buffer.log(TAG, LogLevel.DEBUG, { int1 = host }, { "Host (updateHostAttachment): $int1" }) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt similarity index 80% rename from packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt index fbf9294a0a377ba8b631001a0e21f3ddbe16f0c6..11d0be5fc8bf39d6219527df74047f478a2520a3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt @@ -14,20 +14,17 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.mediaprojection import android.os.IBinder import android.os.Parcel import android.os.Parcelable /** - * Class that represents an area that should be captured. - * Currently it has only a launch cookie that represents a task but - * we potentially could add more identifiers e.g. for a pair of tasks. + * Class that represents an area that should be captured. Currently it has only a launch cookie that + * represents a task but we potentially could add more identifiers e.g. for a pair of tasks. */ -data class MediaProjectionCaptureTarget( - val launchCookie: IBinder? -): Parcelable { +data class MediaProjectionCaptureTarget(val launchCookie: IBinder?) : Parcelable { constructor(parcel: Parcel) : this(parcel.readStrongBinder()) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt similarity index 98% rename from packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt index 9e616e2355e29ef54e20ebdf40981f156b648b6d..f1cade7512e29915bb273455ff990830b603e703 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.mediaprojection import android.content.Context import android.media.projection.IMediaProjection diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt similarity index 97% rename from packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt index 88bc064c60f729f36783d189806316267f2a8900..b5d3e913cadbb5f98d6a303ddd9ac5d048388260 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.mediaprojection.appselector import android.app.ActivityOptions import android.content.Intent @@ -46,13 +46,11 @@ import com.android.internal.app.chooser.TargetInfo import com.android.internal.widget.RecyclerView import com.android.internal.widget.RecyclerViewAccessibilityDelegate import com.android.internal.widget.ResolverDrawerLayout -import com.android.systemui.res.R -import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent -import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController -import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler -import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget +import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.AsyncActivityLauncher import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt index 33d9cc36c9b0b6c3a7bc75a6804ca97b3686de05..72aea040ba05956e332e84d309dc17dca560f5ad 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -23,8 +23,6 @@ import android.os.UserHandle import androidx.lifecycle.DefaultLifecycleObserver import com.android.launcher3.icons.IconFactory import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.media.MediaProjectionAppSelectorActivity -import com.android.systemui.media.MediaProjectionPermissionActivity import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader import com.android.systemui.mediaprojection.appselector.data.AppIconLoader @@ -37,6 +35,7 @@ import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRece import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider import com.android.systemui.mediaprojection.devicepolicy.MediaProjectionDevicePolicyModule import com.android.systemui.mediaprojection.devicepolicy.PersonalProfile +import com.android.systemui.mediaprojection.permission.MediaProjectionPermissionActivity import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.policy.ConfigurationController import dagger.Binds diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt similarity index 99% rename from packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt index 64006fe4c265ff0fafc320ec795f346c79d1b4d6..8b437c3225498dc58d283c9f6590f3bb87f06fd1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenrecord +package com.android.systemui.mediaprojection.permission import android.content.Context import android.os.Bundle diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java similarity index 97% rename from packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java rename to packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index 4de6278400b32d2149b63fa1fcf0d3654921e20c..2b56d0cf9f83de5c81e56a83858232603a9b3a96 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.mediaprojection.permission; import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT; import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT; @@ -22,8 +22,8 @@ import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; -import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN; -import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP; +import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.ENTIRE_SCREEN; +import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.SINGLE_APP; import android.annotation.Nullable; import android.app.Activity; @@ -51,13 +51,13 @@ import android.text.style.StyleSpan; import android.util.Log; import android.view.Window; -import com.android.systemui.res.R; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.mediaprojection.MediaProjectionServiceHelper; +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; -import com.android.systemui.screenrecord.MediaProjectionPermissionDialog; -import com.android.systemui.screenrecord.ScreenShareOption; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.Utils; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt similarity index 98% rename from packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt index 47e28d86e01d7dec6c0cdde6aedb4ca067e463f1..2f10ad3e64862d44e3fa1fcbbcc6b9d36f0a5f25 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenrecord +package com.android.systemui.mediaprojection.permission import android.content.Context import android.media.projection.MediaProjectionConfig diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt similarity index 95% rename from packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt rename to packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt index ebf0dd28fbf452292d548b72ef1bba293628572f..37e8d9f26ee34aff23148f56aa6688a4961b71ba 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenrecord +package com.android.systemui.mediaprojection.permission import androidx.annotation.IntDef import androidx.annotation.StringRes diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 9a9626d7c7a02d5aba2cac4dc6161e2733475470..10f95e0b7201e77d0474ab6a8ab13332fb97d5cd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -48,6 +48,7 @@ import com.android.systemui.qs.nano.QsTileState; import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository; +import com.android.systemui.qs.tiles.di.NewQSTileFactory; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; @@ -56,6 +57,8 @@ import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.settings.SecureSettings; +import dagger.Lazy; + import org.jetbrains.annotations.NotNull; import java.io.PrintWriter; @@ -121,6 +124,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P @Inject public QSTileHost(Context context, + Lazy<NewQSTileFactory> newQsTileFactoryProvider, QSFactory defaultFactory, @Main Executor mainExecutor, PluginManager pluginManager, @@ -147,6 +151,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mShadeController = shadeController; + if (featureFlags.getPipelineTilesEnabled()) { + mQsFactories.add(newQsTileFactoryProvider.get()); + } mQsFactories.add(defaultFactory); pluginManager.addPluginListener(this, QSFactory.class, true); mUserTracker = userTracker; @@ -326,7 +333,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P try { tile = createTile(tileSpec); if (tile != null) { - tile.setTileSpec(tileSpec); if (tile.isAvailable()) { newTiles.put(tileSpec, tile); mQSLogger.logTileAdded(tileSpec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index a6226b36b0fd43d049dd2ecb6878243048c6210e..2af7ae0614ac424001ae7b52a57ea7e19e93ce7e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -130,11 +130,9 @@ public class TileQueryHelper { if (tile == null) { continue; } else if (!tile.isAvailable()) { - tile.setTileSpec(spec); tile.destroy(); continue; } - tile.setTileSpec(spec); tilesToAdd.add(tile); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index 92490e8fbd43195c5338911c09e53079e3142fcf..a65967a0349b616c4ac2aa1b2b5b4f992084a571 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -31,6 +31,7 @@ import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.external.QSExternalModule; import com.android.systemui.qs.pipeline.dagger.QSPipelineModule; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.policy.CastController; @@ -41,14 +42,14 @@ import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.SecureSettings; -import java.util.Map; - -import javax.inject.Named; - import dagger.Module; import dagger.Provides; import dagger.multibindings.Multibinds; +import java.util.Map; + +import javax.inject.Named; + /** * Module for QS dependencies */ @@ -68,6 +69,11 @@ public interface QSModule { @Multibinds Map<String, QSTileImpl<?>> tileMap(); + /** A map of internal QS tile ViewModels. Ensures that this can be injected even if + * it is empty */ + @Multibinds + Map<String, QSTileViewModel> tileViewModelMap(); + @Provides @SysUISingleton static AutoTileManager provideAutoTileManager( diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 00c23582f72664ceba722a7ba0db8119b7dabbd2..c5512c15ccc2205331008106a0662d131ae620c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -41,10 +41,12 @@ import com.android.systemui.qs.pipeline.domain.model.TileModel import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.tiles.di.NewQSTileFactory import com.android.systemui.qs.toProto import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise +import dagger.Lazy import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -131,6 +133,7 @@ constructor( private val installedTilesComponentRepository: InstalledTilesComponentRepository, private val userRepository: UserRepository, private val customTileStatePersister: CustomTileStatePersister, + private val newQSTileFactory: Lazy<NewQSTileFactory>, private val tileFactory: QSFactory, private val customTileAddedRepository: CustomTileAddedRepository, private val tileLifecycleManagerFactory: TileLifecycleManager.Factory, @@ -139,7 +142,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, private val logger: QSPipelineLogger, - featureFlags: QSPipelineFlagsRepository, + private val featureFlags: QSPipelineFlagsRepository, ) : CurrentTilesInteractor { private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> = @@ -333,12 +336,19 @@ constructor( } private suspend fun createTile(spec: TileSpec): QSTile? { - val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) } + val tile = + withContext(mainDispatcher) { + if (featureFlags.pipelineTilesEnabled) { + newQSTileFactory.get().createTile(spec.spec) + } else { + null + } + ?: tileFactory.createTile(spec.spec) + } if (tile == null) { logger.logTileNotFoundInFactory(spec) return null } else { - tile.tileSpec = spec.spec return if (!tile.isAvailable) { logger.logTileDestroyed( spec, diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt index 551b0f4890a4f4400de6ba19c0860b55d5ed2531..1a71b715fe3a7f2cb79869da3a01d5dcbb22f35c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt @@ -1,7 +1,7 @@ package com.android.systemui.qs.pipeline.shared import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject @@ -10,7 +10,7 @@ import javax.inject.Inject class QSPipelineFlagsRepository @Inject constructor( - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlagsClassic, ) { /** @see Flags.QS_PIPELINE_NEW_HOST */ @@ -20,4 +20,8 @@ constructor( /** @see Flags.QS_PIPELINE_AUTO_ADD */ val pipelineAutoAddEnabled: Boolean get() = pipelineHostEnabled && featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD) + + /** @see Flags.QS_PIPELINE_NEW_TILES */ + val pipelineTilesEnabled: Boolean + get() = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_TILES) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt index 11b5dd7cb03676c092fce114a496107daa9e7c8d..aed08f8b5457b61e8699fa788838bb4179e774e3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -31,11 +31,7 @@ import com.android.systemui.qs.external.CustomTile sealed class TileSpec private constructor(open val spec: String) { /** Represents a spec that couldn't be parsed into a valid type of tile. */ - object Invalid : TileSpec("") { - override fun toString(): String { - return "TileSpec.INVALID" - } - } + data object Invalid : TileSpec("") /** Container for the spec of a tile provided by SystemUI. */ data class PlatformTileSpec diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 9c7a734125184f4dbd4f6dffd6cc51132b4ea78e..632aa6330a18fa9002fd4ed3857eb74c6e618d15 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -70,6 +70,7 @@ public class QSFactoryImpl implements QSFactory { if (tile != null) { tile.initialize(); tile.postStale(); // Tile was just created, must be stale. + tile.setTileSpec(tileSpec); } return tile; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt index e9f907c4d8e77baa756788c8d7bd4e8ff61492e2..dc9e11567676415aab86690f6f2ccd81b83ad64e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt @@ -1,11 +1,11 @@ package com.android.systemui.qs.tiles.base.actions import android.content.Intent +import android.view.View import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import javax.inject.Inject /** @@ -17,9 +17,9 @@ class QSTileIntentUserActionHandler @Inject constructor(private val activityStarter: ActivityStarter) { - fun handle(userAction: QSTileUserAction, intent: Intent) { + fun handle(view: View?, intent: Intent) { val animationController: ActivityLaunchAnimator.Controller? = - userAction.view?.let { + view?.let { ActivityLaunchAnimator.Controller.fromView( it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt index c2a75fac60f54548d8edbace31c482320eecf0a4..bb4de808de79a3ecff1d1b224209f3b8aedab1f3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt @@ -92,7 +92,7 @@ constructor( .stateIn( tileScope, SharingStarted.WhileSubscribed(), - false, + true, ) private var currentLifeState: QSTileLifecycle = QSTileLifecycle.DEAD diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..3fedbfc6671d71955e22b13eba69bb21fc3a5a5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt @@ -0,0 +1,28 @@ +package com.android.systemui.qs.tiles.di + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle +import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel +import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter +import javax.inject.Inject +import javax.inject.Provider + +// TODO(b/http://b/299909989): Rename the factory after rollout +@SysUISingleton +class NewQSTileFactory +@Inject +constructor( + private val adapterFactory: QSTileViewModelAdapter.Factory, + private val tileMap: + Map<String, @JvmSuppressWildcards Provider<@JvmSuppressWildcards QSTileViewModel>>, +) : QSFactory { + + override fun createTile(tileSpec: String): QSTile? = + tileMap[tileSpec]?.let { + val tile = it.get() + tile.onLifecycle(QSTileLifecycle.ALIVE) + adapterFactory.create(tile) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt index a5eaac154230750e2ed5ba8a83d13dc7887f70b3..019d3c0ee4162c68292c9ba8ebece065b21d1516 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt @@ -1,14 +1,13 @@ package com.android.systemui.qs.tiles.viewmodel -import android.graphics.drawable.Icon +import androidx.annotation.StringRes +import com.android.internal.logging.InstanceId +import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.pipeline.shared.TileSpec data class QSTileConfig( val tileSpec: TileSpec, val tileIcon: Icon, - val tileLabel: CharSequence, -// TODO(b/299908705): Fill necessary params -/* -val instanceId: InstanceId, - */ + @StringRes val tileLabelRes: Int, + val instanceId: InstanceId, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index 53f9edfb954c8074833edb974a1c067284c8798e..dc5c69080fd02304225903ab73ac9949beb92bb3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -1,18 +1,96 @@ package com.android.systemui.qs.tiles.viewmodel -import android.graphics.drawable.Icon +import android.service.quicksettings.Tile +import com.android.systemui.common.shared.model.Icon +/** + * Represents current a state of the tile to be displayed in on the view. Consider using + * [QSTileState.build] for better state creation experience and preset default values for certain + * fields. + * + * // TODO(b/http://b/299909989): Clean up legacy mappings after the transition + */ data class QSTileState( - val icon: Icon, + val icon: () -> Icon, val label: CharSequence, -// TODO(b/299908705): Fill necessary params -/* - val subtitle: CharSequence = "", - val activeState: ActivationState = Active, - val enabledState: Enabled = Enabled, - val loopIconAnimation: Boolean = false, - val secondaryIcon: Icon? = null, - val slashState: SlashState? = null, - val supportedActions: Collection<UserAction> = listOf(Click), clicks should be a default action -*/ -) + val activationState: ActivationState, + val secondaryLabel: CharSequence?, + val supportedActions: Set<UserAction>, + val contentDescription: CharSequence?, + val stateDescription: CharSequence?, + val sideViewIcon: SideViewIcon, + val enabledState: EnabledState, + val expandedAccessibilityClassName: String?, +) { + + companion object { + + fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState = + Builder(icon, label).apply(build).build() + + fun build(icon: Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState = + build({ icon }, label, build) + } + + enum class ActivationState(val legacyState: Int) { + // An unavailable state indicates that for some reason this tile is not currently available + // to the user, and will have no click action. The tile's icon will be tinted differently to + // reflect this state. + UNAVAILABLE(Tile.STATE_UNAVAILABLE), + // This represents a tile that is currently active. (e.g. wifi is connected, bluetooth is + // on, cast is casting). This is the default state. + ACTIVE(Tile.STATE_ACTIVE), + // This represents a tile that is currently in a disabled state but is still interactable. A + // disabled state indicates that the tile is not currently active (e.g. wifi disconnected or + // bluetooth disabled), but is still interactable by the user to modify this state. + INACTIVE(Tile.STATE_INACTIVE), + } + + /** + * Enabled tile behaves as usual where is disabled one is frozen and inactive in its current + * [ActivationState]. + */ + enum class EnabledState { + ENABLED, + DISABLED, + } + + enum class UserAction { + CLICK, + LONG_CLICK, + } + + sealed interface SideViewIcon { + data class Custom(val icon: Icon) : SideViewIcon + data object Chevron : SideViewIcon + data object None : SideViewIcon + } + + class Builder( + var icon: () -> Icon, + var label: CharSequence, + ) { + var activationState: ActivationState = ActivationState.INACTIVE + var secondaryLabel: CharSequence? = null + var supportedActions: Set<UserAction> = setOf(UserAction.CLICK) + var contentDescription: CharSequence? = null + var stateDescription: CharSequence? = null + var sideViewIcon: SideViewIcon = SideViewIcon.None + var enabledState: EnabledState = EnabledState.ENABLED + var expandedAccessibilityClassName: String? = null + + fun build(): QSTileState = + QSTileState( + icon, + label, + activationState, + secondaryLabel, + supportedActions, + contentDescription, + stateDescription, + sideViewIcon, + enabledState, + expandedAccessibilityClassName, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt index f1f8f0152c677a4ba943bdebdaac2437f43f7186..0b232c28d3f4d766e3f4399dc7d283e0173f707b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt @@ -1,13 +1,11 @@ package com.android.systemui.qs.tiles.viewmodel -import android.content.Context import android.view.View sealed interface QSTileUserAction { - val context: Context val view: View? - class Click(override val context: Context, override val view: View?) : QSTileUserAction - class LongClick(override val context: Context, override val view: View?) : QSTileUserAction + class Click(override val view: View?) : QSTileUserAction + class LongClick(override val view: View?) : QSTileUserAction } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4bdb77c2b41d998cf38407b9c0320b69761bd08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -0,0 +1,232 @@ +package com.android.systemui.qs.tiles.viewmodel + +import android.content.Context +import android.util.Log +import android.view.View +import androidx.annotation.GuardedBy +import com.android.internal.logging.InstanceId +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.function.Supplier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.collectIndexed +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +// TODO(b/http://b/299909989): Use QSTileViewModel directly after the rollout +class QSTileViewModelAdapter +@AssistedInject +constructor( + private val qsHost: QSHost, + @Assisted private val qsTileViewModel: QSTileViewModel, +) : QSTile { + + private val context + get() = qsHost.context + + @GuardedBy("callbacks") + private val callbacks: MutableCollection<QSTile.Callback> = mutableSetOf() + @GuardedBy("listeningClients") + private val listeningClients: MutableCollection<Any> = mutableSetOf() + + // Cancels the jobs when the adapter is no longer alive + private val adapterScope = CoroutineScope(SupervisorJob()) + // Cancels the jobs when clients stop listening + private val listeningScope = CoroutineScope(SupervisorJob()) + + init { + adapterScope.launch { + qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> + if (!isAvailable) { + qsHost.removeTile(tileSpec) + } + // qsTileViewModel.isAvailable flow often starts with isAvailable == true. That's + // why we only allow isAvailable == true once and throw an exception afterwards. + if (index > 0 && isAvailable) { + // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for additional + // guidance on how to auto add your tile + throw UnsupportedOperationException("Turning on tile is not supported now") + } + } + } + + // QSTileHost doesn't call this when userId is initialized + userSwitch(qsHost.userId) + + if (DEBUG) { + Log.d(TAG, "Using new tiles for: $tileSpec") + } + } + + override fun isAvailable(): Boolean = qsTileViewModel.isAvailable.value + + override fun setTileSpec(tileSpec: String?) { + throw UnsupportedOperationException("Tile spec is immutable in new tiles") + } + + override fun refreshState() { + qsTileViewModel.forceUpdate() + } + + override fun addCallback(callback: QSTile.Callback?) { + callback ?: return + synchronized(callbacks) { callbacks.add(callback) } + } + + override fun removeCallback(callback: QSTile.Callback?) { + callback ?: return + synchronized(callbacks) { callbacks.remove(callback) } + } + + override fun removeCallbacks() { + synchronized(callbacks) { callbacks.clear() } + } + + override fun click(view: View?) { + if (isActionSupported(QSTileState.UserAction.CLICK)) { + qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view)) + } + } + + override fun secondaryClick(view: View?) { + if (isActionSupported(QSTileState.UserAction.CLICK)) { + qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view)) + } + } + + override fun longClick(view: View?) { + if (isActionSupported(QSTileState.UserAction.LONG_CLICK)) { + qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(view)) + } + } + + private fun isActionSupported(action: QSTileState.UserAction): Boolean = + qsTileViewModel.currentState?.supportedActions?.contains(action) == true + + override fun userSwitch(currentUser: Int) { + qsTileViewModel.onUserIdChanged(currentUser) + } + + @Deprecated( + "Not needed as {@link com.android.internal.logging.UiEvent} will use #getMetricsSpec", + replaceWith = ReplaceWith("getMetricsSpec"), + ) + override fun getMetricsCategory(): Int = 0 + + override fun setListening(client: Any?, listening: Boolean) { + client ?: return + synchronized(listeningClients) { + if (listening) { + listeningClients.add(client) + if (listeningClients.size == 1) { + qsTileViewModel.state + .map { mapState(context, it, qsTileViewModel.config) } + .onEach { legacyState -> + synchronized(callbacks) { + callbacks.forEach { it.onStateChanged(legacyState) } + } + } + .launchIn(listeningScope) + } + } else { + listeningClients.remove(client) + if (listeningClients.isEmpty()) { + listeningScope.coroutineContext.cancelChildren() + } + } + } + } + + override fun isListening(): Boolean = + synchronized(listeningClients) { listeningClients.isNotEmpty() } + + override fun setDetailListening(show: Boolean) { + // do nothing like QSTileImpl + } + + override fun destroy() { + adapterScope.cancel() + listeningScope.cancel() + qsTileViewModel.onLifecycle(QSTileLifecycle.DEAD) + } + + override fun getState(): QSTile.State? = + qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } + + override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId + override fun getTileLabel(): CharSequence = + context.getString(qsTileViewModel.config.tileLabelRes) + override fun getTileSpec(): String = qsTileViewModel.config.tileSpec.spec + + private companion object { + + const val DEBUG = false + const val TAG = "QSTileVMAdapter" + + fun mapState( + context: Context, + viewModelState: QSTileState, + config: QSTileConfig + ): QSTile.State = + // we have to use QSTile.BooleanState to support different side icons + // which are bound to instanceof QSTile.BooleanState in QSTileView. + QSTile.BooleanState().apply { + spec = config.tileSpec.spec + label = viewModelState.label + // This value is synthetic and doesn't have any meaning + value = false + + secondaryLabel = viewModelState.secondaryLabel + handlesLongClick = + viewModelState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) + + iconSupplier = Supplier { + when (val stateIcon = viewModelState.icon()) { + is Icon.Loaded -> DrawableIcon(stateIcon.drawable) + is Icon.Resource -> ResourceIcon.get(stateIcon.res) + } + } + state = viewModelState.activationState.legacyState + + contentDescription = viewModelState.contentDescription + stateDescription = viewModelState.stateDescription + + disabledByPolicy = viewModelState.enabledState == QSTileState.EnabledState.DISABLED + expandedAccessibilityClassName = viewModelState.expandedAccessibilityClassName + + when (viewModelState.sideViewIcon) { + is QSTileState.SideViewIcon.Custom -> { + sideViewCustomDrawable = + when (viewModelState.sideViewIcon.icon) { + is Icon.Loaded -> viewModelState.sideViewIcon.icon.drawable + is Icon.Resource -> + context.getDrawable(viewModelState.sideViewIcon.icon.res) + } + } + is QSTileState.SideViewIcon.Chevron -> { + forceExpandIcon = true + } + is QSTileState.SideViewIcon.None -> { + forceExpandIcon = false + } + } + } + } + + @AssistedFactory + interface Factory { + + fun create(qsTileViewModel: QSTileViewModel): QSTileViewModelAdapter + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 7cdb6553c47ebf5725484ba4ec9c480f613610e6..3501b6bc045e29ec40d241380677bdc81905108e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -41,10 +41,10 @@ import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.media.MediaProjectionCaptureTarget; +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; +import com.android.systemui.res.R; import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index b80a01212ca0a96cb97544bb52af79e049ee5a86..3aab3bf628095d9e6cc9be540d7d9227517a79d5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -51,7 +51,7 @@ import android.util.Size; import android.view.Surface; import android.view.WindowManager; -import com.android.systemui.media.MediaProjectionCaptureTarget; +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; import java.io.Closeable; import java.io.File; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index f0ce8a465d0d872aff76d9d444bc6008f30cb743..f2e94e94757fb239f977ed94d7d44ee26cd792be 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -18,7 +18,7 @@ package com.android.systemui.screenrecord; import static android.app.Activity.RESULT_OK; -import static com.android.systemui.media.MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET; +import static com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET; import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; @@ -41,8 +41,8 @@ import android.widget.TextView; import androidx.annotation.Nullable; +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; import com.android.systemui.res.R; -import com.android.systemui.media.MediaProjectionCaptureTarget; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.SystemUIDialog; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index b5b7043e37a08415a1b501660fafe4e8371abd11..a1d5d98ba9e92b78b893537b82deb20d3d80faa9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -32,10 +32,14 @@ import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.Switch import androidx.annotation.LayoutRes -import com.android.systemui.res.R -import com.android.systemui.media.MediaProjectionAppSelectorActivity -import com.android.systemui.media.MediaProjectionCaptureTarget +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity +import com.android.systemui.mediaprojection.permission.BaseScreenSharePermissionDialog +import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN +import com.android.systemui.mediaprojection.permission.SINGLE_APP +import com.android.systemui.mediaprojection.permission.ScreenShareOption import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider /** Dialog to select screen recording options */ @@ -58,6 +62,7 @@ class ScreenRecordPermissionDialog( private lateinit var tapsView: View private lateinit var audioSwitch: Switch private lateinit var options: Spinner + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setDialogTitle(R.string.screenrecord_permission_dialog_title) @@ -177,6 +182,7 @@ class ScreenRecordPermissionDialog( ) private const val DELAY_MS: Long = 3000 private const val INTERVAL_MS: Long = 1000 + private fun createOptionList(): List<ScreenShareOption> { return listOf( ScreenShareOption( diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index b7152370624eab790537415472dedd0237725ddb..6fa592c6dd7817d7b923b6bb5e08bd78baca177b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -30,12 +30,13 @@ import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.Gefingerpoken; -import com.android.systemui.res.R; import com.android.systemui.classifier.Classifier; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.haptics.slider.SeekableSliderEventProducer; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.res.R; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.util.ViewController; @@ -283,6 +284,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final VibratorHelper mVibratorHelper; private final SystemClock mSystemClock; private final CoroutineDispatcher mMainDispatcher; + private final ActivityStarter mActivityStarter; @Inject public Factory( @@ -291,14 +293,15 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV VibratorHelper vibratorHelper, SystemClock clock, FeatureFlagsClassic featureFlags, - @Main CoroutineDispatcher mainDispatcher - ) { + @Main CoroutineDispatcher mainDispatcher, + ActivityStarter activityStarter) { mFalsingManager = falsingManager; mUiEventLogger = uiEventLogger; mFeatureFlags = featureFlags; mVibratorHelper = vibratorHelper; mSystemClock = clock; mMainDispatcher = mainDispatcher; + mActivityStarter = activityStarter; } /** @@ -314,6 +317,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV int layout = getLayout(); BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context) .inflate(layout, viewRoot, false); + root.setActivityStarter(mActivityStarter); + BrightnessSliderHapticPlugin plugin; if (mFeatureFlags.isEnabled(HAPTIC_BRIGHTNESS_SLIDER)) { plugin = new BrightnessSliderHapticPluginImpl( diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java index c8854922418393a1f4fd0ee208e8e41600b7bbf6..5ecf07f5a264c6150509bf232c6fec4ecb2c6568 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.Gefingerpoken; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.res.R; /** @@ -41,6 +42,7 @@ import com.android.systemui.res.R; */ public class BrightnessSliderView extends FrameLayout { + private ActivityStarter mActivityStarter; @NonNull private ToggleSeekBar mSlider; private DispatchTouchEventListener mListener; @@ -57,6 +59,10 @@ public class BrightnessSliderView extends FrameLayout { super(context, attrs); } + public void setActivityStarter(@NonNull ActivityStarter activityStarter) { + mActivityStarter = activityStarter; + } + // Inflated from quick_settings_brightness_dialog @Override protected void onFinishInflate() { @@ -65,6 +71,7 @@ public class BrightnessSliderView extends FrameLayout { mSlider = requireViewById(R.id.slider); mSlider.setAccessibilityLabel(getContentDescription().toString()); + mSlider.setActivityStarter(mActivityStarter); // Finds the progress drawable. Assumes brightness_progress_drawable.xml try { diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java index a5a0ae70045eade966d51645567640ed62e360cd..6ec10da28000bac91ded6a1e333b806bd9b589b8 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java @@ -23,8 +23,9 @@ import android.view.MotionEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.SeekBar; +import androidx.annotation.NonNull; + import com.android.settingslib.RestrictedLockUtils; -import com.android.systemui.Dependency; import com.android.systemui.plugins.ActivityStarter; public class ToggleSeekBar extends SeekBar { @@ -32,6 +33,8 @@ public class ToggleSeekBar extends SeekBar { private RestrictedLockUtils.EnforcedAdmin mEnforcedAdmin = null; + private ActivityStarter mActivityStarter; + public ToggleSeekBar(Context context) { super(context); } @@ -49,7 +52,7 @@ public class ToggleSeekBar extends SeekBar { if (mEnforcedAdmin != null) { Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( mContext, mEnforcedAdmin); - Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0); + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); return true; } if (!isEnabled()) { @@ -74,4 +77,8 @@ public class ToggleSeekBar extends SeekBar { public void setEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin) { mEnforcedAdmin = admin; } + + public void setActivityStarter(@NonNull ActivityStarter activityStarter) { + mActivityStarter = activityStarter; + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 251cc168161c317959a5b7a33f12446bdb7a663e..ac8333ae84adc5d2c23a883c4b6e68a8f06e71e9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -129,6 +129,9 @@ constructor( combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) } .stateIn(scope, SharingStarted.Eagerly, 0f) + /** Whether either the shade or QS is fully expanded. */ + val isAnyFullyExpanded: Flow<Boolean> = anyExpansion.map { it >= 1f }.distinctUntilChanged() + /** Whether either the shade or QS is expanding from a fully collapsed state. */ val isAnyExpanding: Flow<Boolean> = anyExpansion diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 8baab25e5c590ec152045af72eac21d6a9ee010b..f616b91c47120ee4055b4ddc6aae48cde7106620 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -244,12 +244,16 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi } final float scaledImageWidth = drawableWidth * scaleToFitIconView; final float scaledImageHeight = drawableHeight * scaleToFitIconView; - // if the scaled image size <= mOriginalStatusBarIconSize, we don't need to enlarge it scaleToOriginalDrawingSize = Math.min( (float) mOriginalStatusBarIconSize / scaledImageWidth, (float) mOriginalStatusBarIconSize / scaledImageHeight); if (scaleToOriginalDrawingSize > 1.0f) { - scaleToOriginalDrawingSize = 1.0f; + // per b/296026932, if the scaled image size <= mOriginalStatusBarIconSize, we need + // to scale up the scaled image to fit in mOriginalStatusBarIconSize. But if both + // the raw drawable intrinsic width/height are less than mOriginalStatusBarIconSize, + // then we just scale up the scaled image back to the raw drawable size. + scaleToOriginalDrawingSize = Math.min( + scaleToOriginalDrawingSize, 1f / scaleToFitIconView); } } iconScale = scaleToOriginalDrawingSize; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt index efa092bb3f40661aeffe58eba8f837db863a32e8..250fe53a2df6fb5de32eb36ab775c62837d151bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt @@ -22,11 +22,12 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION -import com.android.systemui.res.R +import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger +import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.WifiIcons import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -90,50 +91,56 @@ sealed interface WifiIcon : Diffable<WifiIcon> { )}" ) ) - is WifiNetworkModel.Active -> { - val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[model.level]) - val contentDescription = - ContentDescription.Loaded( - if (model.isValidated) { - (levelDesc) - } else { - "$levelDesc,${context.getString(NO_INTERNET)}" - } - ) - Visible(model.toIcon(showHotspotInfo), contentDescription) - } + is WifiNetworkModel.Active -> model.toIcon(showHotspotInfo, context) } - @DrawableRes - private fun WifiNetworkModel.Active.toIcon(showHotspotInfo: Boolean): Int { - return if (!showHotspotInfo) { - this.toBasicIcon() + private fun WifiNetworkModel.Active.toIcon( + showHotspotInfo: Boolean, + context: Context, + ): Visible { + return if ( + !showHotspotInfo || + this.hotspotDeviceType == WifiNetworkModel.HotspotDeviceType.NONE + ) { + this.toBasicIcon(context) } else { - when (this.hotspotDeviceType) { - WifiNetworkModel.HotspotDeviceType.NONE -> this.toBasicIcon() - WifiNetworkModel.HotspotDeviceType.TABLET -> - com.android.settingslib.R.drawable.ic_hotspot_tablet - WifiNetworkModel.HotspotDeviceType.LAPTOP -> - com.android.settingslib.R.drawable.ic_hotspot_laptop - WifiNetworkModel.HotspotDeviceType.WATCH -> - com.android.settingslib.R.drawable.ic_hotspot_watch - WifiNetworkModel.HotspotDeviceType.AUTO -> - com.android.settingslib.R.drawable.ic_hotspot_auto - // Use phone as the default drawable - WifiNetworkModel.HotspotDeviceType.PHONE, - WifiNetworkModel.HotspotDeviceType.UNKNOWN, - WifiNetworkModel.HotspotDeviceType.INVALID -> - com.android.settingslib.R.drawable.ic_hotspot_phone - } + val icon = + when (this.hotspotDeviceType) { + WifiNetworkModel.HotspotDeviceType.TABLET -> + com.android.settingslib.R.drawable.ic_hotspot_tablet + WifiNetworkModel.HotspotDeviceType.LAPTOP -> + com.android.settingslib.R.drawable.ic_hotspot_laptop + WifiNetworkModel.HotspotDeviceType.WATCH -> + com.android.settingslib.R.drawable.ic_hotspot_watch + WifiNetworkModel.HotspotDeviceType.AUTO -> + com.android.settingslib.R.drawable.ic_hotspot_auto + // Use phone as the default drawable + WifiNetworkModel.HotspotDeviceType.PHONE, + WifiNetworkModel.HotspotDeviceType.UNKNOWN, + WifiNetworkModel.HotspotDeviceType.INVALID -> + com.android.settingslib.R.drawable.ic_hotspot_phone + WifiNetworkModel.HotspotDeviceType.NONE -> + throw IllegalStateException("NONE checked earlier") + } + Visible( + icon, + ContentDescription.Loaded(context.getString(WIFI_OTHER_DEVICE_CONNECTION)), + ) } } - @DrawableRes - private fun WifiNetworkModel.Active.toBasicIcon(): Int { + private fun WifiNetworkModel.Active.toBasicIcon(context: Context): Visible { + val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level]) return if (this.isValidated) { - WifiIcons.WIFI_FULL_ICONS[this.level] + Visible( + WifiIcons.WIFI_FULL_ICONS[this.level], + ContentDescription.Loaded(levelDesc), + ) } else { - WifiIcons.WIFI_NO_INTERNET_ICONS[this.level] + Visible( + WifiIcons.WIFI_NO_INTERNET_ICONS[this.level], + ContentDescription.Loaded("$levelDesc,${context.getString(NO_INTERNET)}"), + ) } } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java index ba3dbf0a0cb7b8c1151540fdc804de0dda3aaa31..484b1194eb8b1a406fae7b193d2ab720dd56b813 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java @@ -20,8 +20,10 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.animation.LayoutTransition; import android.view.View; import android.view.ViewTreeObserver; +import android.widget.FrameLayout; import com.android.internal.jank.InteractionJankMonitor; import com.android.keyguard.logging.KeyguardLogger; @@ -44,6 +46,7 @@ import org.mockito.MockitoAnnotations; public class KeyguardStatusViewControllerBaseTest extends SysuiTestCase { @Mock protected KeyguardStatusView mKeyguardStatusView; + @Mock protected KeyguardSliceViewController mKeyguardSliceViewController; @Mock protected KeyguardClockSwitchController mKeyguardClockSwitchController; @Mock protected KeyguardStateController mKeyguardStateController; @@ -61,6 +64,10 @@ public class KeyguardStatusViewControllerBaseTest extends SysuiTestCase { protected KeyguardStatusViewController mController; + @Mock protected KeyguardClockSwitch mKeyguardClockSwitch; + @Mock protected FrameLayout mMediaHostContainer; + @Mock protected LayoutTransition mMediaLayoutTransition; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -93,6 +100,8 @@ public class KeyguardStatusViewControllerBaseTest extends SysuiTestCase { }; when(mKeyguardStatusView.getViewTreeObserver()).thenReturn(mViewTreeObserver); + + when(mKeyguardClockSwitchController.getView()).thenReturn(mKeyguardClockSwitch); } protected void givenViewAttached() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index b8b0198f94dfc8f6cd426e9320f1d464d9a6a495..e4e2b0a8d89f1b79efc291dbfef3d96f48cd1ed0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -18,14 +18,18 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +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.animation.LayoutTransition; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import com.android.systemui.res.R; import com.android.systemui.plugins.ClockConfig; import com.android.systemui.plugins.ClockController; import com.android.systemui.statusbar.notification.AnimatableProperty; @@ -124,4 +128,112 @@ public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControll mController.onDestroy(); verify(mDumpManager, times(1)).unregisterDumpable(eq(mController.getInstanceName())); } + + @Test + public void onInit_addsOnLayoutChangeListenerToClockSwitch() { + when(mKeyguardStatusView.findViewById(R.id.status_view_media_container)).thenReturn( + mMediaHostContainer); + + mController.onInit(); + + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mKeyguardClockSwitch).addOnLayoutChangeListener(captor.capture()); + } + + @Test + public void onInit_addsOnLayoutChangeListenerToMediaHostContainer() { + when(mKeyguardStatusView.findViewById(R.id.status_view_media_container)).thenReturn( + mMediaHostContainer); + + mController.onInit(); + + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mMediaHostContainer).addOnLayoutChangeListener(captor.capture()); + } + + @Test + public void clockSwitchHeightChanged_mediaChangingLayoutTransitionEnabled() { + when(mKeyguardStatusView.findViewById(R.id.status_view_media_container)).thenReturn( + mMediaHostContainer); + + mController.onInit(); + + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mKeyguardClockSwitch).addOnLayoutChangeListener(captor.capture()); + + // Above here is the same as `onInit_addsOnLayoutChangeListenerToClockSwitch`. + // Below here is the actual test. + + View.OnLayoutChangeListener listener = captor.getValue(); + + mController.setSplitShadeEnabled(true); + when(mKeyguardClockSwitch.getSplitShadeCentered()).thenReturn(false); + when(mKeyguardUpdateMonitor.isKeyguardVisible()).thenReturn(true); + when(mMediaHostContainer.getVisibility()).thenReturn(View.VISIBLE); + when(mMediaHostContainer.getHeight()).thenReturn(200); + when(mMediaHostContainer.getLayoutTransition()).thenReturn(mMediaLayoutTransition); + + when(mKeyguardClockSwitch.getHeight()).thenReturn(0); + listener.onLayoutChange(mKeyguardClockSwitch, /* left= */ 0, /* top= */ 0, /* right= */ + 0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */ + 0, /* oldBottom = */ 200); + verify(mMediaLayoutTransition).enableTransitionType(LayoutTransition.CHANGING); + } + + @Test + public void clockSwitchHeightNotChanged_mediaChangingLayoutTransitionNotEnabled() { + when(mKeyguardStatusView.findViewById(R.id.status_view_media_container)).thenReturn( + mMediaHostContainer); + + mController.onInit(); + + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mKeyguardClockSwitch).addOnLayoutChangeListener(captor.capture()); + + // Above here is the same as `onInit_addsOnLayoutChangeListenerToClockSwitch`. + // Below here is the actual test. + + View.OnLayoutChangeListener listener = captor.getValue(); + + mController.setSplitShadeEnabled(true); + when(mKeyguardClockSwitch.getSplitShadeCentered()).thenReturn(false); + when(mKeyguardUpdateMonitor.isKeyguardVisible()).thenReturn(true); + when(mMediaHostContainer.getVisibility()).thenReturn(View.VISIBLE); + when(mMediaHostContainer.getHeight()).thenReturn(200); + when(mMediaHostContainer.getLayoutTransition()).thenReturn(mMediaLayoutTransition); + + when(mKeyguardClockSwitch.getHeight()).thenReturn(200); + listener.onLayoutChange(mKeyguardClockSwitch, /* left= */ 0, /* top= */ 0, /* right= */ + 0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */ + 0, /* oldBottom = */ 200); + verify(mMediaLayoutTransition, never()).enableTransitionType(LayoutTransition.CHANGING); + } + + @Test + public void onMediaHostContainerLayout_disablesChangingLayoutTransition() { + when(mKeyguardStatusView.findViewById(R.id.status_view_media_container)).thenReturn( + mMediaHostContainer); + + mController.onInit(); + + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mMediaHostContainer).addOnLayoutChangeListener(captor.capture()); + + // Above here is the same as `onInit_addsOnLayoutChangeListenerToMediaHostContainer`. + // Below here is the actual test. + + View.OnLayoutChangeListener listener = captor.getValue(); + + when(mMediaHostContainer.getLayoutTransition()).thenReturn(mMediaLayoutTransition); + + when(mMediaLayoutTransition.isTransitionTypeEnabled(LayoutTransition.CHANGING)).thenReturn( + true); + listener.onLayoutChange(mMediaHostContainer, 1, 2, 3, 4, 1, 2, 3, 4); + verify(mMediaLayoutTransition).disableTransitionType(LayoutTransition.CHANGING); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt index 86439e557f8b4da0a1c48375599461aaf5b95a52..58d372c68c55440c6092029bd8936575ba768a5b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt @@ -1,5 +1,6 @@ package com.android.keyguard +import android.animation.LayoutTransition import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -34,6 +35,20 @@ class KeyguardStatusViewTest : SysuiTestCase() { as KeyguardStatusView } + @Test + fun mediaViewHasLayoutTransitionInDisabledState() { + val layoutTransition = (mediaView as ViewGroup).layoutTransition + assertThat(layoutTransition).isNotNull() + assertThat(layoutTransition.isTransitionTypeEnabled(LayoutTransition.CHANGE_APPEARING)) + .isFalse() + assertThat(layoutTransition.isTransitionTypeEnabled(LayoutTransition.CHANGE_DISAPPEARING)) + .isFalse() + assertThat(layoutTransition.isTransitionTypeEnabled(LayoutTransition.APPEARING)).isFalse() + assertThat(layoutTransition.isTransitionTypeEnabled(LayoutTransition.DISAPPEARING)) + .isFalse() + assertThat(layoutTransition.isTransitionTypeEnabled(LayoutTransition.CHANGING)).isFalse() + } + @Test fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() { val translationY = 1234f diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 25faeef85e500fa3ae284838561b57e47a4bd03a..de57b603c7fe835d248a9ea2b04975470113c9df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -81,6 +81,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController + @Mock lateinit var logger: MediaViewLogger @Captor private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)> @Captor @@ -121,7 +122,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() { notifPanelEvents, settings, fakeHandler, - ResourcesSplitShadeStateController() + ResourcesSplitShadeStateController(), + logger, ) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index b595e8de3bad5f4189b88057193c9f26e5efa645..79411f427f1fbae91e3082fddd76d48407961ae2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -66,6 +66,7 @@ import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.di.NewQSTileFactory; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; @@ -77,6 +78,8 @@ import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -101,8 +104,6 @@ public class QSTileHostTest extends SysuiTestCase { private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE); private static final String SETTING = QSHost.TILES_SETTING; - @Mock - private QSFactory mDefaultFactory; @Mock private PluginManager mPluginManager; @Mock @@ -117,7 +118,6 @@ public class QSTileHostTest extends SysuiTestCase { private CustomTile mCustomTile; @Mock private UserTracker mUserTracker; - private SecureSettings mSecureSettings; @Mock private CustomTileStatePersister mCustomTileStatePersister; @Mock @@ -127,6 +127,10 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private UserFileManager mUserFileManager; + private SecureSettings mSecureSettings; + + private QSFactory mDefaultFactory; + private SparseArray<SharedPreferences> mSharedPreferencesByUser; private FakeFeatureFlags mFeatureFlags; @@ -144,6 +148,8 @@ public class QSTileHostTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false); mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false); + // TODO(b/299909337): Add test checking the new factory is used when the flag is on + mFeatureFlags.set(Flags.QS_PIPELINE_NEW_TILES, false); mQSPipelineFlagsRepository = new QSPipelineFlagsRepository(mFeatureFlags); mMainExecutor = new FakeExecutor(new FakeSystemClock()); @@ -164,7 +170,8 @@ public class QSTileHostTest extends SysuiTestCase { mSecureSettings = new FakeSettings(); saveSetting(""); - mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor, + setUpTileFactory(); + mQSTileHost = new TestQSTileHost(mContext, () -> null, mDefaultFactory, mMainExecutor, mPluginManager, mTunerService, () -> mAutoTiles, mShadeController, mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, mTileLifecycleManagerFactory, mUserFileManager, mQSPipelineFlagsRepository); @@ -178,7 +185,6 @@ public class QSTileHostTest extends SysuiTestCase { mMainExecutor.runAllReady(); } }, mUserTracker.getUserId()); - setUpTileFactory(); } private void saveSetting(String value) { @@ -191,32 +197,29 @@ public class QSTileHostTest extends SysuiTestCase { } private void setUpTileFactory() { - // Only create this kind of tiles - when(mDefaultFactory.createTile(anyString())).thenAnswer( - invocation -> { - String spec = invocation.getArgument(0); - if ("spec1".equals(spec)) { - return new TestTile1(mQSTileHost); - } else if ("spec2".equals(spec)) { - return new TestTile2(mQSTileHost); - } else if ("spec3".equals(spec)) { - return new TestTile3(mQSTileHost); - } else if ("na".equals(spec)) { - return new NotAvailableTile(mQSTileHost); - } else if (CUSTOM_TILE_SPEC.equals(spec)) { - QSTile tile = mCustomTile; - QSTile.State s = mock(QSTile.State.class); - s.spec = spec; - when(mCustomTile.getState()).thenReturn(s); - return tile; - } else if ("internet".equals(spec) - || "wifi".equals(spec) - || "cell".equals(spec)) { - return new TestTile1(mQSTileHost); - } else { - return null; - } - }); + mDefaultFactory = new FakeQSFactory(spec -> { + if ("spec1".equals(spec)) { + return new TestTile1(mQSTileHost); + } else if ("spec2".equals(spec)) { + return new TestTile2(mQSTileHost); + } else if ("spec3".equals(spec)) { + return new TestTile3(mQSTileHost); + } else if ("na".equals(spec)) { + return new NotAvailableTile(mQSTileHost); + } else if (CUSTOM_TILE_SPEC.equals(spec)) { + QSTile tile = mCustomTile; + QSTile.State s = mock(QSTile.State.class); + s.spec = spec; + when(mCustomTile.getState()).thenReturn(s); + return tile; + } else if ("internet".equals(spec) + || "wifi".equals(spec) + || "cell".equals(spec)) { + return new TestTile1(mQSTileHost); + } else { + return null; + } + }); when(mCustomTile.isAvailable()).thenReturn(true); } @@ -703,7 +706,7 @@ public class QSTileHostTest extends SysuiTestCase { } private class TestQSTileHost extends QSTileHost { - TestQSTileHost(Context context, + TestQSTileHost(Context context, Lazy<NewQSTileFactory> newQSTileFactoryProvider, QSFactory defaultFactory, Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, @@ -712,7 +715,7 @@ public class QSTileHostTest extends SysuiTestCase { CustomTileStatePersister customTileStatePersister, TileLifecycleManager.Factory tileLifecycleManagerFactory, UserFileManager userFileManager, QSPipelineFlagsRepository featureFlags) { - super(context, defaultFactory, mainExecutor, pluginManager, + super(context, newQSTileFactoryProvider, defaultFactory, mainExecutor, pluginManager, tunerService, autoTiles, shadeController, qsLogger, userTracker, secureSettings, customTileStatePersister, tileLifecycleManagerFactory, userFileManager, featureFlags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java index bde30382ba05d988d1fd44ecae44c1817f350f37..d3cd26bcd3cb9d8bca4d794c16f675fb5cc4ed38 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java @@ -124,6 +124,7 @@ public class TileQueryHelperTest extends SysuiTestCase { if (FACTORY_TILES.contains(spec)) { FakeQSTile tile = new FakeQSTile(mBgExecutor, mMainExecutor); tile.setState(mState); + tile.setTileSpec(spec); return tile; } else { return null; @@ -284,7 +285,10 @@ public class TileQueryHelperTest extends SysuiTestCase { Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null); QSTile t = mock(QSTile.class); - when(mQSHost.createTile("hotspot")).thenReturn(t); + when(mQSHost.createTile("hotspot")).thenAnswer(invocation -> { + t.setTileSpec("hotspot"); + return t; + }); mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, "hotspot"); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index dc1b9c4d0d14864c13e0fada8f55631f4e1a9020..a7505240caeb704977cb9fb3a71846786fd07cb5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -46,6 +46,7 @@ import com.android.systemui.qs.pipeline.domain.model.TileModel import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.tiles.di.NewQSTileFactory import com.android.systemui.qs.toProto import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.FakeUserRepository @@ -91,6 +92,8 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { @Mock private lateinit var logger: QSPipelineLogger + @Mock private lateinit var newQSTileFactory: NewQSTileFactory + private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -105,6 +108,8 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true) featureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, true) + // TODO(b/299909337): Add test checking the new factory is used when the flag is on + featureFlags.set(Flags.QS_PIPELINE_NEW_TILES, true) userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) @@ -117,6 +122,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { userRepository = userRepository, customTileStatePersister = customTileStatePersister, tileFactory = tileFactory, + newQSTileFactory = { newQSTileFactory }, customTileAddedRepository = customTileAddedRepository, tileLifecycleManagerFactory = tileLifecycleManagerFactory, userTracker = userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt index 5630b9d3b2929c8c1a894eef4e03602246b1c00a..2e6b50b637dd1b978cff5de36866d129c924a2a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt @@ -1,6 +1,6 @@ package com.android.systemui.qs.pipeline.domain.interactor -import android.testing.AndroidTestingRunner +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase @@ -21,7 +21,7 @@ import org.junit.runner.RunWith import org.mockito.MockitoAnnotations @RoboPilotTest -@RunWith(AndroidTestingRunner::class) +@RunWith(AndroidJUnit4::class) @SmallTest class RestoreReconciliationInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt index 47b4244e0910eda49aeefa386feef04ebfa2dd7e..077c81343c83b1aedf8d85e90bfd7d5b87ff94ce 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt @@ -6,7 +6,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserActionHandler -import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,7 +33,7 @@ class QSTileIntentUserActionHandlerTest : SysuiTestCase() { fun testPassesIntentToStarter() { val intent = Intent("test.ACTION") - underTest.handle(QSTileUserAction.Click(context, null), intent) + underTest.handle(null, intent) verify(activityStarted).postStartActivityDismissingKeyguard(eq(intent), eq(0), any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt index 643866e3cadeb891e47f549b550db07cb05c24ff..eacb08010159255567868eb1cc934fd6d8fcafc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt @@ -1,11 +1,14 @@ package com.android.systemui.qs.tiles.viewmodel -import android.graphics.drawable.Icon +import android.graphics.drawable.ShapeDrawable import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.MediumTest +import com.android.internal.logging.InstanceId import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor @@ -71,20 +74,21 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { fakeQSTileUserActionInteractor, fakeQSTileDataInteractor, object : QSTileDataToStateMapper<Any> { - override fun map(config: QSTileConfig, data: Any): QSTileState { - return QSTileState(config.tileIcon, config.tileLabel) - } + override fun map(config: QSTileConfig, data: Any): QSTileState = + QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} }, testCoroutineDispatcher, tileScope = scope.backgroundScope, ) {} private companion object { + val TEST_QS_TILE_CONFIG = QSTileConfig( TileSpec.create("default"), - Icon.createWithContentUri(""), - "", + Icon.Loaded(ShapeDrawable(), null), + 0, + InstanceId.fakeInstanceId(0), ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index 59b59539374900969f0a18161e855abc108b4fde..a2aed988a423b01743d6fd9e2a3b108710cba86a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -41,7 +41,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaProjectionCaptureTarget; +import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.SysuiStatusBarStateController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt index d470d24489af1d2856d95cbf8acff3b20119ef19..3ae1f35b8134e0866bd3379f39e544fdb77f967c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt @@ -22,11 +22,13 @@ import android.testing.TestableLooper import android.view.View import android.widget.Spinner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN +import com.android.systemui.mediaprojection.permission.SINGLE_APP import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -35,8 +37,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java index 1592c3007ec837265ed966ae0a3ea4282ceadf82..a6180ec8ea0eecbfa170a3cd903cdf843ec101f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java @@ -248,7 +248,7 @@ public class StatusBarIconViewTest extends SysuiTestCase { } @Test - public void testUpdateIconScale_smallerFontAndConstrainedDrawableSizeLessThanDpIconSize() { + public void testUpdateIconScale_smallerFontAndRawDrawableSizeLessThanDpIconSize() { int dpIconSize = 60; int dpDrawingSize = 30; // smaller font scaling causes the spIconSize < dpIconSize @@ -262,12 +262,42 @@ public class StatusBarIconViewTest extends SysuiTestCase { setIconDrawableWithSize(/* width= */ 50, /* height= */ 50); mIconView.maybeUpdateIconScaleDimens(); - // WHEN both the constrained drawable width/height are less than dpIconSize, + // WHEN both the raw/constrained drawable width/height are less than dpIconSize, + // THEN the icon is scaled up from constrained drawable size to the raw drawable size + float scaleToBackRawDrawableSize = (float) 50 / 40; // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; // THEN the scaled icon should be scaled down further to fit spIconSize float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; - assertEquals(scaleToFitDrawingSize * scaleToFitSpIconSize, mIconView.getIconScale(), 0.01f); + assertEquals(scaleToBackRawDrawableSize * scaleToFitDrawingSize * scaleToFitSpIconSize, + mIconView.getIconScale(), 0.01f); + } + + @Test + public void testUpdateIconScale_smallerFontAndConstrainedDrawableSizeLessThanDpIconSize() { + int dpIconSize = 60; + int dpDrawingSize = 30; + // smaller font scaling causes the spIconSize < dpIconSize + int spIconSize = 40; + // the icon view layout size would be 40x150 + // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) + setUpIconView(dpIconSize, dpDrawingSize, spIconSize); + mIconView.setNotification(mock(StatusBarNotification.class)); + // the raw drawable size is 70x70. When put the drawable into iconView whose + // layout size is 40x150, the drawable size would be constrained to 40x40 + setIconDrawableWithSize(/* width= */ 70, /* height= */ 70); + mIconView.maybeUpdateIconScaleDimens(); + + // WHEN the raw drawable width/height are larger than dpIconSize, + // but the constrained drawable width/height are less than dpIconSize, + // THEN the icon is scaled up from constrained drawable size to fit dpIconSize + float scaleToFitDpIconSize = (float) dpIconSize / 40; + // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize + float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; + // THEN the scaled icon should be scaled down further to fit spIconSize + float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; + assertEquals(scaleToFitDpIconSize * scaleToFitDrawingSize * scaleToFitSpIconSize, + mIconView.getIconScale(), 0.01f); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt index 15b9d61fc96bdc3b1f55bc76d784a0902b9651e4..c935dbb0ca1c5aceb1836c456b8bf784949f56b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt @@ -17,13 +17,14 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import androidx.test.filters.SmallTest -import com.android.systemui.res.R +import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon +import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.WifiIcons import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor @@ -181,6 +182,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(WifiIcons.WIFI_NO_INTERNET_ICONS[4])) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .doesNotContain(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -192,6 +195,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_tablet)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -203,6 +208,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_laptop)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -214,6 +221,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_watch)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -225,6 +234,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_auto)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -236,6 +247,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_phone)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -247,6 +260,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_phone)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test @@ -258,6 +273,8 @@ class InternetTileViewModelTest : SysuiTestCase() { assertThat(latest?.icon) .isEqualTo(ResourceIcon.get(com.android.settingslib.R.drawable.ic_hotspot_phone)) + assertThat(latest?.stateDescription.loadContentDescription(context)) + .isEqualTo(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index a520f6c109cca01e32fb58d5efc0cf03b839d147..49a2648a7caca2d93792065cd47d0c8a1455b640 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.connectivity.WifiIcons @@ -136,6 +138,10 @@ class WifiViewModelTest : SysuiTestCase() { // is used instead assertThat(latest).isInstanceOf(WifiIcon.Visible::class.java) assertThat((latest as WifiIcon.Visible).res).isEqualTo(WifiIcons.WIFI_FULL_ICONS[1]) + assertThat( + (latest as WifiIcon.Visible).contentDescription.loadContentDescription(context) + ) + .doesNotContain(context.getString(WIFI_OTHER_DEVICE_CONNECTION)) } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt index bf26e719433db3c555589a89f47e767dae71f295..cbf4ae5e301497436207afbfa4af1f784b059cf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt @@ -21,6 +21,6 @@ import com.android.systemui.plugins.qs.QSTile class FakeQSFactory(private val tileCreator: (String) -> QSTile?) : QSFactory { override fun createTile(tileSpec: String): QSTile? { - return tileCreator(tileSpec) + return tileCreator(tileSpec)?.also { it.tileSpec = tileSpec } } } diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index b56b47f9c727225542533c304c327d9a1b0a6fad..9dd0dca47f0efca42a7bf2ec25db3c7a27940dc0 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -121,7 +121,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @NonNull @GuardedBy("mGenericWindowPolicyControllerLock") - final ArraySet<Integer> mRunningUids = new ArraySet<>(); + private final ArraySet<Integer> mRunningUids = new ArraySet<>(); @Nullable private final ActivityListener mActivityListener; @Nullable private final PipBlockedCallback mPipBlockedCallback; @Nullable private final IntentListenerCallback mIntentListenerCallback; diff --git a/services/core/java/com/android/server/connectivity/OWNERS b/services/core/java/com/android/server/connectivity/OWNERS index 62c5737a2e8e5a7da8dcff019eb36d3c55b94ce4..c24680e9b06a616db26148e4d2ce2e833626daf9 100644 --- a/services/core/java/com/android/server/connectivity/OWNERS +++ b/services/core/java/com/android/server/connectivity/OWNERS @@ -1,2 +1,2 @@ set noparent -file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index debf828abf0ab38ea95296a5c560cde3af2130c2..99a5398aa7eec4cf16a72e3af2cfe894078a0897 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -33,7 +33,6 @@ import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; -import com.android.internal.display.BrightnessUtils; import com.android.internal.util.Preconditions; import com.android.server.display.utils.Plog; import com.android.server.display.whitebalance.DisplayWhiteBalanceController; diff --git a/core/java/com/android/internal/display/BrightnessUtils.java b/services/core/java/com/android/server/display/BrightnessUtils.java similarity index 97% rename from core/java/com/android/internal/display/BrightnessUtils.java rename to services/core/java/com/android/server/display/BrightnessUtils.java index 82b506bed80bce18fafdfb7b50c4c8159c14d57f..84fa0cccbd10b7dd57f883b09018f0f574c52953 100644 --- a/core/java/com/android/internal/display/BrightnessUtils.java +++ b/services/core/java/com/android/server/display/BrightnessUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.internal.display; +package com.android.server.display; import android.util.MathUtils; diff --git a/services/core/java/com/android/server/display/RampAnimator.java b/services/core/java/com/android/server/display/RampAnimator.java index e38c2c58f453c0eb776b3cd7f36316dd67431add..5ba042c51a452c4f754b8885960348ab1a626f0f 100644 --- a/services/core/java/com/android/server/display/RampAnimator.java +++ b/services/core/java/com/android/server/display/RampAnimator.java @@ -20,8 +20,6 @@ import android.animation.ValueAnimator; import android.util.FloatProperty; import android.view.Choreographer; -import com.android.internal.display.BrightnessUtils; - /** * A custom animator that progressively updates a property value at * a given variable rate until it reaches a particular target value. diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java index 41053e908a00a3714398005296c7db8d7f7070ef..68848a2ad426c1e3a046d7e6752172c39e67be51 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java @@ -16,35 +16,65 @@ package com.android.server.grammaticalinflection; +import static android.app.Flags.systemTermsOfAddressEnabled; import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; import android.annotation.Nullable; +import android.app.GrammaticalInflectionManager; import android.app.IGrammaticalInflectionManager; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.os.Binder; -import android.os.IBinder; +import android.os.Environment; import android.os.Process; +import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.SystemProperties; +import android.util.AtomicFile; import android.util.Log; +import android.util.SparseIntArray; +import android.util.Xml; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.XmlUtils; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.wm.ActivityTaskManagerInternal; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + /** * The implementation of IGrammaticalInflectionManager.aidl. * * <p>This service is API entry point for storing app-specific grammatical inflection. */ public class GrammaticalInflectionService extends SystemService { - private final String TAG = "GrammaticalInflection"; + private static final String TAG = "GrammaticalInflection"; + private static final String ATTR_NAME = "grammatical_gender"; + private static final String USER_SETTINGS_FILE_NAME = "user_settings.xml"; + private static final String TAG_GRAMMATICAL_INFLECTION = "grammatical_inflection"; + private static final String GRAMMATICAL_INFLECTION_ENABLED = + "i18n.grammatical_Inflection.enabled"; + private final GrammaticalInflectionBackupHelper mBackupHelper; private final ActivityTaskManagerInternal mActivityTaskManagerInternal; + private final Object mLock = new Object(); + private final SparseIntArray mGrammaticalGenderCache = new SparseIntArray(); + private PackageManagerInternal mPackageManagerInternal; - private static final String GRAMMATICAL_INFLECTION_ENABLED = - "i18n.grammatical_Inflection.enabled"; + private GrammaticalInflectionService.GrammaticalInflectionBinderService mBinderService; /** * Initializes the system service. @@ -62,22 +92,46 @@ public class GrammaticalInflectionService extends SystemService { mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mBackupHelper = new GrammaticalInflectionBackupHelper( this, context.getPackageManager()); + mBinderService = new GrammaticalInflectionBinderService(); } @Override public void onStart() { - publishBinderService(Context.GRAMMATICAL_INFLECTION_SERVICE, mService); + publishBinderService(Context.GRAMMATICAL_INFLECTION_SERVICE, mBinderService); LocalServices.addService(GrammaticalInflectionManagerInternal.class, new GrammaticalInflectionManagerInternalImpl()); } - private final IBinder mService = new IGrammaticalInflectionManager.Stub() { + private final class GrammaticalInflectionBinderService extends + IGrammaticalInflectionManager.Stub { @Override public void setRequestedApplicationGrammaticalGender( String appPackageName, int userId, int gender) { GrammaticalInflectionService.this.setRequestedApplicationGrammaticalGender( appPackageName, userId, gender); } + + @Override + public void setSystemWideGrammaticalGender(int userId, int grammaticalGender) { + checkCallerIsSystem(); + checkSystemTermsOfAddressIsEnabled(); + GrammaticalInflectionService.this.setSystemWideGrammaticalGender(grammaticalGender, + userId); + } + + @Override + public int getSystemGrammaticalGender(int userId) { + checkSystemTermsOfAddressIsEnabled(); + return GrammaticalInflectionService.this.getSystemGrammaticalGender(userId); + } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new GrammaticalInflectionShellCommand(mBinderService)) + .exec(this, in, out, err, args, callback, resultReceiver); + } }; private final class GrammaticalInflectionManagerInternalImpl @@ -94,12 +148,6 @@ public class GrammaticalInflectionService extends SystemService { public void stageAndApplyRestoredPayload(byte[] payload, int userId) { mBackupHelper.stageAndApplyRestoredPayload(payload, userId); } - - private void checkCallerIsSystem() { - if (Binder.getCallingUid() != Process.SYSTEM_UID) { - throw new SecurityException("Caller is not system."); - } - } } protected int getApplicationGrammaticalGender(String appPackageName, int userId) { @@ -137,4 +185,105 @@ public class GrammaticalInflectionService extends SystemService { updater.setGrammaticalGender(gender).commit(); } + + protected void setSystemWideGrammaticalGender(int grammaticalGender, int userId) { + if (!GrammaticalInflectionManager.VALID_GRAMMATICAL_GENDER_VALUES.contains( + grammaticalGender)) { + throw new IllegalArgumentException("Unknown grammatical gender"); + } + + synchronized (mLock) { + final File file = getGrammaticalGenderFile(userId); + final AtomicFile atomicFile = new AtomicFile(file); + FileOutputStream stream = null; + try { + stream = atomicFile.startWrite(); + stream.write(toXmlByteArray(grammaticalGender, stream)); + atomicFile.finishWrite(stream); + mGrammaticalGenderCache.put(userId, grammaticalGender); + } catch (IOException e) { + Log.e(TAG, "Failed to write file " + atomicFile, e); + if (stream != null) { + atomicFile.failWrite(stream); + } + throw new RuntimeException(e); + } + } + } + + // TODO(b/298591009): Add a new AppOp value for the apps that want to access the grammatical + // gender. + public int getSystemGrammaticalGender(int userId) { + synchronized (mLock) { + final File file = getGrammaticalGenderFile(userId); + if (!file.exists()) { + Log.d(TAG, "User " + userId + "doesn't have the grammatical gender file."); + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + if (mGrammaticalGenderCache.indexOfKey(userId) < 0) { + try { + InputStream in = new FileInputStream(file); + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + mGrammaticalGenderCache.put(userId, getGrammaticalGenderFromXml(parser)); + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Failed to parse XML configuration from " + file, e); + } + } + return mGrammaticalGenderCache.get(userId); + } + } + + private File getGrammaticalGenderFile(int userId) { + final File dir = new File(Environment.getDataSystemCeDirectory(userId), + TAG_GRAMMATICAL_INFLECTION); + return new File(dir, USER_SETTINGS_FILE_NAME); + } + + private byte[] toXmlByteArray(int grammaticalGender, FileOutputStream fileStream) { + + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + TypedXmlSerializer out = Xml.resolveSerializer(fileStream); + out.setOutput(outputStream, StandardCharsets.UTF_8.name()); + out.startDocument(/* encoding= */ null, /* standalone= */ true); + out.startTag(null, TAG_GRAMMATICAL_INFLECTION); + out.attributeInt(null, ATTR_NAME, grammaticalGender); + out.endTag(null, TAG_GRAMMATICAL_INFLECTION); + out.endDocument(); + + return outputStream.toByteArray(); + } catch (IOException e) { + return null; + } + } + + private int getGrammaticalGenderFromXml(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + + XmlUtils.nextElement(parser); + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + String tagName = parser.getName(); + if (TAG_GRAMMATICAL_INFLECTION.equals(tagName)) { + return parser.getAttributeInt(null, ATTR_NAME); + } else { + XmlUtils.nextElement(parser); + } + } + + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + private void checkCallerIsSystem() { + int callingUid = Binder.getCallingUid(); + if (callingUid != Process.SYSTEM_UID && callingUid != Process.SHELL_UID) { + throw new SecurityException("Caller is not system and shell."); + } + } + + private void checkSystemTermsOfAddressIsEnabled() { + if (!systemTermsOfAddressEnabled()) { + throw new RuntimeException("The flag must be enabled to allow calling the API."); + } + } } diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..d22372860eadd88e5be58982c2a280a7c50d3bea --- /dev/null +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 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.grammaticalinflection; + +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + +import android.app.ActivityManager; +import android.app.GrammaticalInflectionManager; +import android.app.IGrammaticalInflectionManager; +import android.content.res.Configuration; +import android.os.RemoteException; +import android.os.ShellCommand; +import android.os.UserHandle; +import android.util.SparseArray; + +import java.io.PrintWriter; + +/** + * Shell commands for {@link GrammaticalInflectionService} + */ +class GrammaticalInflectionShellCommand extends ShellCommand { + + private static final SparseArray<String> GRAMMATICAL_GENDER_MAP = new SparseArray<>(); + static { + GRAMMATICAL_GENDER_MAP.put(Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED, + "Not specified (0)"); + GRAMMATICAL_GENDER_MAP.put(Configuration.GRAMMATICAL_GENDER_NEUTRAL, "Neuter (1)"); + GRAMMATICAL_GENDER_MAP.put(Configuration.GRAMMATICAL_GENDER_FEMININE, "Feminine (2)"); + GRAMMATICAL_GENDER_MAP.put(Configuration.GRAMMATICAL_GENDER_MASCULINE, "Masculine (3)"); + } + + private final IGrammaticalInflectionManager mBinderService; + + GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager) { + mBinderService = grammaticalInflectionManager; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + switch (cmd) { + case "set-system-grammatical-gender": + return runSetSystemWideGrammaticalGender(); + case "get-system-grammatical-gender": + return runGetSystemGrammaticalGender(); + default: { + return handleDefaultCommands(cmd); + } + } + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Grammatical inflection manager (grammatical_inflection) shell commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println( + " set-system-grammatical-gender [--user <USER_ID>] [--grammaticalGender " + + "<GRAMMATICAL_GENDER>]"); + pw.println(" Set the system grammatical gender for system."); + pw.println(" --user <USER_ID>: apply for the given user, " + + "the current user is used when unspecified."); + pw.println( + " --grammaticalGender <GRAMMATICAL_GENDER>: The terms of address the user " + + "preferred in system, not specified (0) is used when unspecified."); + pw.println( + " eg. 0 = not_specified, 1 = neuter, 2 = feminine, 3 = masculine" + + "."); + pw.println( + " get-system-grammatical-gender [--user <USER_ID>]"); + pw.println(" Get the system grammatical gender for system."); + pw.println(" --user <USER_ID>: apply for the given user, " + + "the current user is used when unspecified."); + } + + private int runSetSystemWideGrammaticalGender() { + int userId = ActivityManager.getCurrentUser(); + int grammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED; + do { + String option = getNextOption(); + if (option == null) { + break; + } + switch (option) { + case "--user": { + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + } + case "-g": + case "--grammaticalGender": { + grammaticalGender = parseGrammaticalGender(); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + option); + } + } + } while (true); + + try { + mBinderService.setSystemWideGrammaticalGender(userId, grammaticalGender); + } catch (RemoteException e) { + getOutPrintWriter().println("Remote Exception: " + e); + } + return 0; + } + + private int runGetSystemGrammaticalGender() { + int userId = ActivityManager.getCurrentUser(); + do { + String option = getNextOption(); + if (option == null) { + break; + } + switch (option) { + case "--user": { + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + option); + } + } + } while (true); + + try { + int grammaticalGender = mBinderService.getSystemGrammaticalGender(userId); + getOutPrintWriter().println(GRAMMATICAL_GENDER_MAP.get(grammaticalGender)); + } catch (RemoteException e) { + getOutPrintWriter().println("Remote Exception: " + e); + } + return 0; + } + + private int parseGrammaticalGender() { + String arg = getNextArg(); + if (arg == null) { + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } else { + int grammaticalGender = Integer.parseInt(arg); + if (GrammaticalInflectionManager.VALID_GRAMMATICAL_GENDER_VALUES.contains( + grammaticalGender)) { + return grammaticalGender; + } else { + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + } + } +} diff --git a/services/core/java/com/android/server/grammaticalinflection/OWNERS b/services/core/java/com/android/server/grammaticalinflection/OWNERS index 5f16ba9123b7737342770ac6ec6f8ee3646e39f5..41d079ed9e75ab1636982d7bf87fe52a18ebce5e 100644 --- a/services/core/java/com/android/server/grammaticalinflection/OWNERS +++ b/services/core/java/com/android/server/grammaticalinflection/OWNERS @@ -2,3 +2,4 @@ allenwtsu@google.com goldmanj@google.com calvinpan@google.com +zoeychen@google.com diff --git a/services/core/java/com/android/server/net/OWNERS b/services/core/java/com/android/server/net/OWNERS index 9c96d46f15b8c3be86ad009cf7f8a51d7be733bc..d0e95dd55b6c4dc448cdf669f3bad37f38c5db5a 100644 --- a/services/core/java/com/android/server/net/OWNERS +++ b/services/core/java/com/android/server/net/OWNERS @@ -1,5 +1,5 @@ set noparent -file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking jsharkey@android.com sudheersai@google.com diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index 04d1da61f3cc77d987b76f46970828b9e2effd4d..390c45b6a524026eb6a2557901bd4f8dd62c9472 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -145,6 +145,19 @@ "include-filter": "android.content.pm.cts.PackageManagerShellCommandMultiUserTest" } ] + }, + { + "file_patterns": [ + "(/|^)InstallPackageHelper\\.java", + "services/core/java/com/android/server/pm/parsing/.*", + "services/core/java/com/android/server/pm/pkg/parsing/.*" + ], + "name": "SdkSandboxManagerServiceUnitTests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] } ], "imports": [ diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index dfc9b8b030f2fcd0869979aa44cf63b7e1bc7f43..097656cac7f7b135cbb2f0312754764dcd5e8257 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -198,7 +198,6 @@ import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; -import com.android.internal.display.BrightnessUtils; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; @@ -218,6 +217,7 @@ import com.android.server.GestureLauncherService; import com.android.server.LocalServices; import com.android.server.SystemServiceManager; import com.android.server.UiThread; +import com.android.server.display.BrightnessUtils; import com.android.server.input.InputManagerInternal; import com.android.server.input.KeyboardMetricsCollector; import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; diff --git a/services/core/java/com/android/server/security/rkp/OWNERS b/services/core/java/com/android/server/security/rkp/OWNERS index 348f940483111b84262f9522284b6b02ba23c1e9..ea6dc727c7b2d3677266288ee4c835f012a5e424 100644 --- a/services/core/java/com/android/server/security/rkp/OWNERS +++ b/services/core/java/com/android/server/security/rkp/OWNERS @@ -1 +1 @@ -file:platform/frameworks/base:master:/core/java/android/security/rkp/OWNERS +file:platform/frameworks/base:main:/core/java/android/security/rkp/OWNERS diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 16847243b6e15602cac184139c27541d70985b45..ed10346d8495be606e458c531eb4a7fc0484a2f6 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5366,7 +5366,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Finish should only ever commit visibility=false, so we can check full containment // rather than just direct membership. inFinishingTransition = mTransitionController.inFinishingTransition(this); - if (!inFinishingTransition && !mDisplayContent.isSleeping()) { + if (!inFinishingTransition && (visible || !mDisplayContent.isSleeping())) { Slog.e(TAG, "setVisibility=" + visible + " while transition is not collecting or finishing " + this + " caller=" + Debug.getCallers(8)); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 5d95bc77edcbccf4e2a61ab998aaff82ae8944ed..50bc825d8beac263ef87314977eaef188d9b2469 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -254,6 +254,11 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ boolean mClearedForReorderActivityToFront; + /** + * Whether the TaskFragment surface is managed by a system {@link TaskFragmentOrganizer}. + */ + boolean mIsSurfaceManagedBySystemOrganizer = false; + /** * When we are in the process of pausing an activity, before starting the * next one, this variable holds the activity that is currently being paused. @@ -449,13 +454,21 @@ class TaskFragment extends WindowContainer<WindowContainer> { void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid, @NonNull String processName) { + setTaskFragmentOrganizer(organizer, uid, processName, + false /* isSurfaceManagedBySystemOrganizer */); + } + + void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid, + @NonNull String processName, boolean isSurfaceManagedBySystemOrganizer) { mTaskFragmentOrganizer = ITaskFragmentOrganizer.Stub.asInterface(organizer.asBinder()); mTaskFragmentOrganizerUid = uid; mTaskFragmentOrganizerProcessName = processName; + mIsSurfaceManagedBySystemOrganizer = isSurfaceManagedBySystemOrganizer; } void onTaskFragmentOrganizerRemoved() { mTaskFragmentOrganizer = null; + mIsSurfaceManagedBySystemOrganizer = false; } /** Whether this TaskFragment is organized by the given {@code organizer}. */ @@ -2396,6 +2409,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (mDelayOrganizedTaskFragmentSurfaceUpdate || mTaskFragmentOrganizer == null) { return; } + if (mIsSurfaceManagedBySystemOrganizer) { + return; + } if (mTransitionController.isShellTransitionsEnabled() && !mTransitionController.isCollecting(this)) { // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index ea722b61be6fc85dc014fb187ef4f3f121d79d50..04164c20a3725ad638b3122d10af92ab969b19b1 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -26,6 +26,7 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_I import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; +import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer; @@ -50,12 +51,15 @@ import android.window.ITaskFragmentOrganizer; import android.window.ITaskFragmentOrganizerController; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOperation; +import android.window.TaskFragmentOrganizerToken; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -132,6 +136,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private final Map<IBinder, ActivityRecord> mTemporaryActivityTokens = new WeakHashMap<>(); + /** + * Whether this {@link android.window.TaskFragmentOrganizer} is a system organizer. If true, + * the {@link android.view.SurfaceControl} of the {@link TaskFragment} is provided to the + * client in the {@link TYPE_TASK_FRAGMENT_APPEARED} event. + */ + private final boolean mIsSystemOrganizer; + /** * {@link RemoteAnimationDefinition} for embedded activities transition animation that is * organized by this organizer. @@ -147,10 +158,12 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr */ private final ArrayMap<IBinder, Integer> mDeferredTransitions = new ArrayMap<>(); - TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer, int pid, int uid) { + TaskFragmentOrganizerState(@NonNull ITaskFragmentOrganizer organizer, int pid, int uid, + boolean isSystemOrganizer) { mOrganizer = organizer; mOrganizerPid = pid; mOrganizerUid = uid; + mIsSystemOrganizer = isSystemOrganizer; try { mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/); } catch (RemoteException e) { @@ -235,11 +248,15 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr tf.mTaskFragmentAppearedSent = true; mLastSentTaskFragmentInfos.put(tf, info); mTaskFragmentTaskIds.put(tf, taskId); - return new TaskFragmentTransaction.Change( + final TaskFragmentTransaction.Change change = new TaskFragmentTransaction.Change( TYPE_TASK_FRAGMENT_APPEARED) .setTaskFragmentToken(tf.getFragmentToken()) .setTaskFragmentInfo(info) .setTaskId(taskId); + if (mIsSystemOrganizer) { + change.setTaskFragmentSurfaceControl(tf.getSurfaceControl()); + } + return change; } @NonNull @@ -435,8 +452,25 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr : null; } + @VisibleForTesting + void registerOrganizer(@NonNull ITaskFragmentOrganizer organizer) { + registerOrganizerInternal(organizer, false /* isSystemOrganizer */); + } + @Override - public void registerOrganizer(@NonNull ITaskFragmentOrganizer organizer) { + public void registerOrganizer( + @NonNull ITaskFragmentOrganizer organizer, boolean isSystemOrganizer) { + registerOrganizerInternal( + organizer, + Flags.taskFragmentSystemOrganizerFlag() && isSystemOrganizer); + } + + @VisibleForTesting + void registerOrganizerInternal( + @NonNull ITaskFragmentOrganizer organizer, boolean isSystemOrganizer) { + if (isSystemOrganizer) { + enforceTaskPermission("registerSystemOrganizer()"); + } final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); synchronized (mGlobalLock) { @@ -448,7 +482,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr "Replacing existing organizer currently unsupported"); } mTaskFragmentOrganizerState.put(organizer.asBinder(), - new TaskFragmentOrganizerState(organizer, pid, uid)); + new TaskFragmentOrganizerState(organizer, pid, uid, isSystemOrganizer)); mPendingTaskFragmentEvents.put(organizer.asBinder(), new ArrayList<>()); } } @@ -711,6 +745,12 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } + boolean isSystemOrganizer(@NonNull TaskFragmentOrganizerToken token) { + final TaskFragmentOrganizerState state = + mTaskFragmentOrganizerState.get(token.asBinder()); + return state != null && state.mIsSystemOrganizer; + } + @Nullable private PendingTaskFragmentEvent getLastPendingParentInfoChangedEvent( @NonNull ITaskFragmentOrganizer organizer, @NonNull Task task) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 6d7e2970e2b1f1e8e79bba6fa9135a4e1eaebfb0..376cad7348662aef6d59917dd3e53cbcd78fd793 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -99,6 +99,7 @@ import android.window.IWindowOrganizerController; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentOperation; +import android.window.TaskFragmentOrganizerToken; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -1993,8 +1994,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub creationParams.getFragmentToken(), true /* createdByOrganizer */); // Set task fragment organizer immediately, since it might have to be notified about further // actions. - taskFragment.setTaskFragmentOrganizer(creationParams.getOrganizer(), - ownerActivity.getUid(), ownerActivity.info.processName); + TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer(); + taskFragment.setTaskFragmentOrganizer(organizerToken, + ownerActivity.getUid(), ownerActivity.info.processName, + mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken)); final int position; if (creationParams.getPairedPrimaryFragmentToken() != null) { // When there is a paired primary TaskFragment, we want to place the new TaskFragment diff --git a/services/net/OWNERS b/services/net/OWNERS index 62c5737a2e8e5a7da8dcff019eb36d3c55b94ce4..c24680e9b06a616db26148e4d2ce2e833626daf9 100644 --- a/services/net/OWNERS +++ b/services/net/OWNERS @@ -1,2 +1,2 @@ set noparent -file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking diff --git a/services/tests/RemoteProvisioningServiceTests/OWNERS b/services/tests/RemoteProvisioningServiceTests/OWNERS index 348f940483111b84262f9522284b6b02ba23c1e9..ea6dc727c7b2d3677266288ee4c835f012a5e424 100644 --- a/services/tests/RemoteProvisioningServiceTests/OWNERS +++ b/services/tests/RemoteProvisioningServiceTests/OWNERS @@ -1 +1 @@ -file:platform/frameworks/base:master:/core/java/android/security/rkp/OWNERS +file:platform/frameworks/base:main:/core/java/android/security/rkp/OWNERS diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java index 7a4327cc05980c655a600acff492190f7e014284..2fd6e5fb68926995927b2df35b4cd4270c145e09 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java @@ -19,7 +19,6 @@ package com.android.server.display; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.isA; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -28,18 +27,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.ContextWrapper; import android.database.ContentObserver; -import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.net.Uri; import android.os.Handler; -import android.os.PowerManager; import android.os.UserHandle; import android.os.test.TestLooper; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.view.Display; -import android.view.DisplayAdjustments; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; @@ -63,7 +59,6 @@ public class BrightnessSynchronizerTest { private static final float EPSILON = 0.00001f; private static final Uri BRIGHTNESS_URI = Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS); - private static final float BRIGHTNESS_MAX = 0.6f; private Context mContext; private MockContentResolver mContentResolverSpy; @@ -71,7 +66,6 @@ public class BrightnessSynchronizerTest { private DisplayListener mDisplayListener; private ContentObserver mContentObserver; private TestLooper mTestLooper; - private BrightnessSynchronizer mSynchronizer; @Mock private DisplayManager mDisplayManagerMock; @Captor private ArgumentCaptor<DisplayListener> mDisplayListenerCaptor; @@ -80,17 +74,7 @@ public class BrightnessSynchronizerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - - Display display = mock(Display.class); - when(display.getDisplayAdjustments()).thenReturn(new DisplayAdjustments()); - BrightnessInfo info = new BrightnessInfo(PowerManager.BRIGHTNESS_INVALID_FLOAT, - PowerManager.BRIGHTNESS_MIN, BRIGHTNESS_MAX, - BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, BRIGHTNESS_MAX, - BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE); - when(display.getBrightnessInfo()).thenReturn(info); - - mContext = spy(new ContextWrapper( - ApplicationProvider.getApplicationContext().createDisplayContext(display))); + mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mContentResolverSpy = spy(new MockContentResolver(mContext)); mContentResolverSpy.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); when(mContext.getContentResolver()).thenReturn(mContentResolverSpy); @@ -144,12 +128,13 @@ public class BrightnessSynchronizerTest { @Test public void testSetSameIntValue_nothingUpdated() { putFloatSetting(0.5f); + putIntSetting(128); start(); - putIntSetting(fToI(0.5f)); + putIntSetting(128); advanceTime(10); verify(mDisplayManagerMock, times(0)).setBrightness( - eq(Display.DEFAULT_DISPLAY), eq(0.5f)); + eq(Display.DEFAULT_DISPLAY), eq(iToF(128))); } @Test @@ -169,13 +154,14 @@ public class BrightnessSynchronizerTest { // Verify that this update did not get sent to float, because synchronizer // is still waiting for confirmation of its first value. verify(mDisplayManagerMock, times(0)).setBrightness( - Display.DEFAULT_DISPLAY, iToF(20)); + eq(Display.DEFAULT_DISPLAY), eq(iToF(20))); // Send the confirmation of the initial change. This should trigger the new value to // finally be processed and we can verify that the new value (20) is sent. putIntSetting(fToI(0.4f)); advanceTime(10); - verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY, iToF(20)); + verify(mDisplayManagerMock).setBrightness( + eq(Display.DEFAULT_DISPLAY), eq(iToF(20))); } @@ -197,7 +183,8 @@ public class BrightnessSynchronizerTest { advanceTime(200); // Verify that the new value gets sent because the timeout expired. - verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY, iToF(20)); + verify(mDisplayManagerMock).setBrightness( + eq(Display.DEFAULT_DISPLAY), eq(iToF(20))); // Send a confirmation of the initial event, BrightnessSynchronizer should treat this as a // new event because the timeout had already expired @@ -209,14 +196,14 @@ public class BrightnessSynchronizerTest { // Verify we sent what would have been the confirmation as a new event to displaymanager. // We do both fToI and iToF because the conversions are not symmetric. - verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY, - iToF(fToI(0.4f))); + verify(mDisplayManagerMock).setBrightness( + eq(Display.DEFAULT_DISPLAY), eq(iToF(fToI(0.4f)))); } - private void start() { - mSynchronizer = new BrightnessSynchronizer(mContext, mTestLooper.getLooper(), + private BrightnessSynchronizer start() { + BrightnessSynchronizer bs = new BrightnessSynchronizer(mContext, mTestLooper.getLooper(), mClock::now); - mSynchronizer.startSynchronizing(); + bs.startSynchronizing(); verify(mDisplayManagerMock).registerDisplayListener(mDisplayListenerCaptor.capture(), isA(Handler.class), eq(DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS)); mDisplayListener = mDisplayListenerCaptor.getValue(); @@ -224,6 +211,7 @@ public class BrightnessSynchronizerTest { verify(mContentResolverSpy).registerContentObserver(eq(BRIGHTNESS_URI), eq(false), mContentObserverCaptor.capture(), eq(UserHandle.USER_ALL)); mContentObserver = mContentObserverCaptor.getValue(); + return bs; } private int getIntSetting() throws Exception { @@ -253,11 +241,11 @@ public class BrightnessSynchronizerTest { } private int fToI(float brightness) { - return mSynchronizer.brightnessFloatToIntSetting(brightness); + return BrightnessSynchronizer.brightnessFloatToInt(brightness); } private float iToF(int brightness) { - return mSynchronizer.brightnessIntSettingToFloat(brightness); + return BrightnessSynchronizer.brightnessIntToFloat(brightness); } private void advanceTime(long timeMs) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 306de525e53cdffdcdd6199a3f103f8f6d5f0119..2396905aecbfdaba16683ab0a3add0de816e3166 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -71,7 +71,6 @@ import android.graphics.Rect; import android.hardware.Sensor; import android.hardware.SensorManager; import android.hardware.display.BrightnessConfiguration; -import android.hardware.display.BrightnessInfo; import android.hardware.display.Curve; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; @@ -95,7 +94,6 @@ import android.os.Process; import android.os.RemoteException; import android.view.ContentRecordingSession; import android.view.Display; -import android.view.DisplayAdjustments; import android.view.DisplayCutout; import android.view.DisplayEventReceiver; import android.view.DisplayInfo; @@ -103,6 +101,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.window.DisplayWindowPolicyController; +import androidx.test.InstrumentationRegistry; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; @@ -333,11 +332,7 @@ public class DisplayManagerServiceTest { LocalServices.removeServiceForTest(UserManagerInternal.class); LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal); // TODO: b/287945043 - Display display = mock(Display.class); - when(display.getDisplayAdjustments()).thenReturn(new DisplayAdjustments()); - when(display.getBrightnessInfo()).thenReturn(mock(BrightnessInfo.class)); - mContext = spy(new ContextWrapper( - ApplicationProvider.getApplicationContext().createDisplayContext(display))); + mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mResources = Mockito.spy(mContext.getResources()); manageDisplaysPermission(/* granted= */ false); when(mContext.getResources()).thenReturn(mResources); @@ -1912,6 +1907,7 @@ public class DisplayManagerServiceTest { @Test public void testSettingTwoBrightnessConfigurationsOnMultiDisplay() { + Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); // get the first two internal displays diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a7c8a6cee0d9d357ac43b0b6987ed58817574768 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java @@ -0,0 +1,890 @@ +/* + * Copyright (C) 2022 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.companion.virtual; + +import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; +import static android.view.WindowManager.LayoutParams.FLAG_SECURE; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.WindowConfiguration; +import android.companion.virtual.IVirtualDeviceIntentInterceptor; +import android.companion.virtual.VirtualDeviceManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.ArraySet; +import android.view.Display; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.app.BlockedAppStreamingActivity; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class GenericWindowPolicyControllerTest { + + private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY + 1; + private static final int TEST_UID = 1234567; + private static final String DISPLAY_CATEGORY = "com.display.category"; + private static final String NONBLOCKED_APP_PACKAGE_NAME = "com.someapp"; + private static final String BLOCKED_PACKAGE_NAME = "com.blockedapp"; + private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000; + private static final String TEST_SITE = "http://test"; + private static final ComponentName BLOCKED_APP_STREAMING_COMPONENT = + new ComponentName("android", BlockedAppStreamingActivity.class.getName()); + private static final ComponentName BLOCKED_COMPONENT = new ComponentName(BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME); + private static final ComponentName NONBLOCKED_COMPONENT = new ComponentName( + NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + private GenericWindowPolicyController.PipBlockedCallback mPipBlockedCallback; + @Mock + private VirtualDeviceManager.ActivityListener mActivityListener; + @Mock + private GenericWindowPolicyController.IntentListenerCallback mIntentListenerCallback; + @Mock + private GenericWindowPolicyController.ActivityBlockedCallback mActivityBlockedCallback; + @Mock + private GenericWindowPolicyController.RunningAppsChangedListener mRunningAppsChangedListener; + @Mock + private GenericWindowPolicyController.SecureWindowCallback mSecureWindowCallback; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void showTasksInHostDeviceRecents() { + GenericWindowPolicyController gwpc = createGwpc(); + + gwpc.setShowInHostDeviceRecents(true); + assertThat(gwpc.canShowTasksInHostDeviceRecents()).isTrue(); + + gwpc.setShowInHostDeviceRecents(false); + assertThat(gwpc.canShowTasksInHostDeviceRecents()).isFalse(); + } + + @Test + public void containsUid() { + GenericWindowPolicyController gwpc = createGwpc(); + + assertThat(gwpc.containsUid(TEST_UID)).isFalse(); + + gwpc.onRunningAppsChanged(new ArraySet<>(Arrays.asList(TEST_UID))); + assertThat(gwpc.containsUid(TEST_UID)).isTrue(); + + gwpc.onRunningAppsChanged(new ArraySet<>()); + assertThat(gwpc.containsUid(TEST_UID)).isFalse(); + } + + @Test + public void isEnteringPipAllowed_falseByDefault() { + GenericWindowPolicyController gwpc = createGwpc(); + + assertThat(gwpc.isEnteringPipAllowed(TEST_UID)).isFalse(); + verify(mPipBlockedCallback).onEnteringPipBlocked(TEST_UID); + } + + @Test + public void isEnteringPipAllowed_dpcSupportsPinned_allowed() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setSupportedWindowingModes(new HashSet<>( + Arrays.asList(WindowConfiguration.WINDOWING_MODE_FULLSCREEN, + WindowConfiguration.WINDOWING_MODE_PINNED))); + assertThat(gwpc.isEnteringPipAllowed(TEST_UID)).isTrue(); + verify(mPipBlockedCallback, never()).onEnteringPipBlocked(TEST_UID); + } + + @Test + public void openNonBlockedAppOnVirtualDisplay_isNotBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void activityDoesNotSupportDisplayOnRemoteDevices_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ false, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void openBlockedComponentOnVirtualDisplay_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent(BLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void addActivityPolicyExemption_openBlockedOnVirtualDisplay_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + gwpc.setActivityLaunchDefaultAllowed(true); + gwpc.addActivityPolicyExemption(BLOCKED_COMPONENT); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void openNotAllowedComponentOnBlocklistVirtualDisplay_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void addActivityPolicyExemption_openNotAllowedOnVirtualDisplay_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + gwpc.setActivityLaunchDefaultAllowed(false); + gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void openAllowedComponentOnBlocklistVirtualDisplay_startsActivity() { + GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void addActivityPolicyExemption_openAllowedOnVirtualDisplay_startsActivity() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + gwpc.setActivityLaunchDefaultAllowed(false); + gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_mismatchingUserHandle_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null, + /* uid */ UserHandle.PER_USER_RANGE + 1); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_blockedAppStreamingComponent_isNeverBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), + BLOCKED_APP_STREAMING_COMPONENT.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_blockedAppStreamingComponentExplicitlyBlocked_isNeverBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent( + BLOCKED_APP_STREAMING_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), + BLOCKED_APP_STREAMING_COMPONENT.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_blockedAppStreamingComponentExemptFromStreaming_isNeverBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + gwpc.setActivityLaunchDefaultAllowed(true); + gwpc.addActivityPolicyExemption(BLOCKED_APP_STREAMING_COMPONENT); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), + BLOCKED_APP_STREAMING_COMPONENT.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_blockedAppStreamingComponentNotAllowlisted_isNeverBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), + BLOCKED_APP_STREAMING_COMPONENT.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_blockedAppStreamingComponentNotExemptFromBlocklist_isNeverBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + gwpc.setActivityLaunchDefaultAllowed(false); + gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_APP_STREAMING_COMPONENT.getPackageName(), + BLOCKED_APP_STREAMING_COMPONENT.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_customDisplayCategoryMatches_isNotBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ DISPLAY_CATEGORY); + + assertActivityCanBeLaunched(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_customDisplayCategoryDoesNotMatch_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ "some.random.category"); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void canActivityBeLaunched_crossTaskLaunch_fromDefaultDisplay_isNotBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, Display.DEFAULT_DISPLAY, true, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + @Test + public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notExplicitlyBlocked_isNotBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor( + BLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertActivityCanBeLaunched(gwpc, DISPLAY_ID, true, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + @Test + public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_explicitlyBlocked_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor( + BLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, DISPLAY_ID, true, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + @Test + public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notAllowed_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed( + NONBLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, DISPLAY_ID, true, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + @Test + public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_allowed_isNotBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed( + NONBLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityCanBeLaunched(gwpc, DISPLAY_ID, true, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + @Test + public void canActivityBeLaunched_unsupportedWindowingMode_isBlocked() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, DISPLAY_ID, true, WindowConfiguration.WINDOWING_MODE_PINNED, + activityInfo); + } + + @Test + public void canActivityBeLaunched_permissionComponent_isBlocked() { + GenericWindowPolicyController gwpc = createGwpcWithPermissionComponent(BLOCKED_COMPONENT); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + BLOCKED_PACKAGE_NAME, + BLOCKED_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertActivityIsBlocked(gwpc, activityInfo); + } + + @Test + public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() { + ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(TEST_UID)); + GenericWindowPolicyController gwpc = createGwpc(); + + gwpc.registerRunningAppsChangedListener(mRunningAppsChangedListener); + gwpc.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(1); + verify(mRunningAppsChangedListener).onRunningAppsChanged(uids); + } + + @Test + public void onRunningAppsChanged_empty_onDisplayEmpty() { + ArraySet<Integer> uids = new ArraySet<>(); + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + gwpc.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); + verify(mActivityListener).onDisplayEmpty(DISPLAY_ID); + } + + @Test + public void noRunningAppsChangedListener_onRunningAppsChanged_doesNotThrowException() { + ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(TEST_UID)); + GenericWindowPolicyController gwpc = createGwpc(); + + gwpc.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); + verify(mRunningAppsChangedListener, never()).onRunningAppsChanged(uids); + } + + @Test + public void registerUnregisterRunningAppsChangedListener_onRunningAppsChanged_doesNotThrowException() { + ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(TEST_UID)); + GenericWindowPolicyController gwpc = createGwpc(); + + gwpc.registerRunningAppsChangedListener(mRunningAppsChangedListener); + gwpc.unregisterRunningAppsChangedListener(mRunningAppsChangedListener); + gwpc.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); + verify(mRunningAppsChangedListener, never()).onRunningAppsChanged(uids); + } + + @Test + public void canActivityBeLaunched_intentInterceptedWhenRegistered_activityNoLaunch() + throws RemoteException { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_SITE)); + + IVirtualDeviceIntentInterceptor.Stub interceptor = + mock(IVirtualDeviceIntentInterceptor.Stub.class); + doNothing().when(interceptor).onIntentIntercepted(any()); + doReturn(interceptor).when(interceptor).asBinder(); + doReturn(interceptor).when(interceptor).queryLocalInterface(anyString()); + + GenericWindowPolicyController gwpc = createGwpc(); + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + // register interceptor and intercept intent + when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(true); + assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isFalse(); + + // unregister interceptor and launch activity + when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(false); + assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isTrue(); + } + + @Test + public void canActivityBeLaunched_noMatchIntentFilter_activityLaunches() { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("testing")); + + GenericWindowPolicyController gwpc = createGwpc(); + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + // register interceptor with different filter + when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(false); + assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isTrue(); + verify(mIntentListenerCallback).shouldInterceptIntent(any(Intent.class)); + } + + @Test + public void onTopActivitychanged_null_noCallback() { + GenericWindowPolicyController gwpc = createGwpc(); + + gwpc.onTopActivityChanged(null, 0, 0); + verify(mActivityListener, never()) + .onTopActivityChanged(anyInt(), any(ComponentName.class), anyInt()); + } + + @Test + public void onTopActivitychanged_activityListenerCallbackObserved() { + int userId = 1000; + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + gwpc.onTopActivityChanged(BLOCKED_COMPONENT, 0, userId); + verify(mActivityListener) + .onTopActivityChanged(eq(DISPLAY_ID), eq(BLOCKED_COMPONENT), eq(userId)); + } + + @Test + public void keepActivityOnWindowFlagsChanged_noChange() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, 0, 0)).isTrue(); + + verify(mSecureWindowCallback, never()).onSecureWindowShown(DISPLAY_ID, + activityInfo.applicationInfo.uid); + verify(mActivityBlockedCallback, never()).onActivityBlocked(DISPLAY_ID, activityInfo); + } + + @Test + public void keepActivityOnWindowFlagsChanged_flagSecure_isAllowedAfterTM() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, FLAG_SECURE, 0)).isTrue(); + + verify(mSecureWindowCallback).onSecureWindowShown(DISPLAY_ID, + activityInfo.applicationInfo.uid); + verify(mActivityBlockedCallback, never()).onActivityBlocked(DISPLAY_ID, activityInfo); + } + + @Test + public void keepActivityOnWindowFlagsChanged_systemFlagHideNonSystemOverlayWindows_isAllowedAfterTM() { + GenericWindowPolicyController gwpc = createGwpc(); + gwpc.setDisplayId(DISPLAY_ID); + + ActivityInfo activityInfo = getActivityInfo( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + + assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, 0, + SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS)).isTrue(); + + verify(mSecureWindowCallback, never()).onSecureWindowShown(DISPLAY_ID, + activityInfo.applicationInfo.uid); + verify(mActivityBlockedCallback, never()).onActivityBlocked(DISPLAY_ID, activityInfo); + } + + @Test + public void getCustomHomeComponent_noneSet() { + GenericWindowPolicyController gwpc = createGwpc(); + + assertThat(gwpc.getCustomHomeComponent()).isNull(); + } + + @Test + public void getCustomHomeComponent_returnsHomeComponent() { + GenericWindowPolicyController gwpc = createGwpcWithCustomHomeComponent( + NONBLOCKED_COMPONENT); + + assertThat(gwpc.getCustomHomeComponent()).isEqualTo(NONBLOCKED_COMPONENT); + } + + private GenericWindowPolicyController createGwpc() { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ mSecureWindowCallback, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithCustomHomeComponent( + ComponentName homeComponent) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ homeComponent); + } + + private GenericWindowPolicyController createGwpcWithBlockedComponent( + ComponentName blockedComponent) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ Collections.singleton(blockedComponent), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithAllowedComponent( + ComponentName allowedComponent) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ false, + /* activityPolicyExemptions= */ Collections.singleton(allowedComponent), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithDisplayCategory( + String displayCategory) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ Collections.singleton(displayCategory), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithCrossTaskNavigationBlockedFor( + ComponentName blockedComponent) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ true, + /* crossTaskNavigationExemptions= */ Collections.singleton(blockedComponent), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithCrossTaskNavigationAllowed( + ComponentName allowedComponent) { + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ false, + /* crossTaskNavigationExemptions= */ Collections.singleton(allowedComponent), + /* permissionDialogComponent= */ null, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private GenericWindowPolicyController createGwpcWithPermissionComponent( + ComponentName permissionComponent) { + //TODO instert the component + return new GenericWindowPolicyController( + 0, + 0, + /* allowedUsers= */ new ArraySet<>(getCurrentUserId()), + /* activityLaunchAllowedByDefault= */ true, + /* activityPolicyExemptions= */ new ArraySet<>(), + /* crossTaskNavigationAllowedByDefault= */ false, + /* crossTaskNavigationExemptions= */ new ArraySet<>(), + /* permissionDialogComponent= */ permissionComponent, + /* activityListener= */ mActivityListener, + /* pipBlockedCallback= */ mPipBlockedCallback, + /* activityBlockedCallback= */ mActivityBlockedCallback, + /* secureWindowCallback= */ null, + /* intentListenerCallback= */ mIntentListenerCallback, + /* displayCategories= */ new ArraySet<>(), + /* showTasksInHostDeviceRecents= */ true, + /* customHomeComponent= */ null); + } + + private Set<UserHandle> getCurrentUserId() { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + return new ArraySet<>(Arrays.asList(context.getUser())); + } + + private ActivityInfo getActivityInfo( + String packageName, String name, boolean displayOnRemoteDevices, + String requiredDisplayCategory) { + return getActivityInfo(packageName, name, displayOnRemoteDevices, requiredDisplayCategory, + 0); + } + + private ActivityInfo getActivityInfo( + String packageName, String name, boolean displayOnRemoteDevices, + String requiredDisplayCategory, int uid) { + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = uid; + + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = packageName; + activityInfo.name = name; + activityInfo.flags = displayOnRemoteDevices + ? FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES : FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES; + activityInfo.applicationInfo = applicationInfo; + activityInfo.requiredDisplayCategory = requiredDisplayCategory; + return activityInfo; + } + + private void assertActivityCanBeLaunched(GenericWindowPolicyController gwpc, + ActivityInfo activityInfo) { + assertActivityCanBeLaunched(gwpc, DISPLAY_ID, false, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + private void assertActivityCanBeLaunched(GenericWindowPolicyController gwpc, int fromDisplay, + boolean isNewTask, int windowingMode, ActivityInfo activityInfo) { + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, windowingMode, fromDisplay, + isNewTask)).isTrue(); + + verify(mActivityBlockedCallback, never()).onActivityBlocked(fromDisplay, activityInfo); + verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); + } + + private void assertActivityIsBlocked(GenericWindowPolicyController gwpc, + ActivityInfo activityInfo) { + assertActivityIsBlocked(gwpc, DISPLAY_ID, false, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, activityInfo); + } + + private void assertActivityIsBlocked(GenericWindowPolicyController gwpc, int fromDisplay, + boolean isNewTask, int windowingMode, ActivityInfo activityInfo) { + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, windowingMode, fromDisplay, + isNewTask)).isFalse(); + + verify(mActivityBlockedCallback).onActivityBlocked(fromDisplay, activityInfo); + verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 1e6306cb4071454c156aed11db36b1a9a5dea65f..263d4701eba144e12dc7f9361efef03bbca0bdcd 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.content.Context.DEVICE_ID_DEFAULT; import static android.content.Context.DEVICE_ID_INVALID; import static android.content.Intent.ACTION_VIEW; import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -70,6 +71,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.hardware.Sensor; import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.DisplayManagerInternal; @@ -303,7 +305,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME, - /* displayOnRemoveDevices= */ true, + /* displayOnRemoteDevices= */ true, targetDisplayCategory); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -314,12 +316,12 @@ public class VirtualDeviceManagerServiceTest { private ActivityInfo getActivityInfo( - String packageName, String name, boolean displayOnRemoveDevices, + String packageName, String name, boolean displayOnRemoteDevices, String requiredDisplayCategory) { ActivityInfo activityInfo = new ActivityInfo(); activityInfo.packageName = packageName; activityInfo.name = name; - activityInfo.flags = displayOnRemoveDevices + activityInfo.flags = displayOnRemoteDevices ? FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES : FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES; activityInfo.applicationInfo = mApplicationInfoMock; activityInfo.requiredDisplayCategory = requiredDisplayCategory; @@ -1427,7 +1429,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1448,7 +1450,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( PERMISSION_CONTROLLER_PACKAGE_NAME, PERMISSION_CONTROLLER_PACKAGE_NAME, - /* displayOnRemoveDevices */ false, + /* displayOnRemoteDevices */ false, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1513,7 +1515,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( SETTINGS_PACKAGE_NAME, SETTINGS_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1534,7 +1536,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( VENDING_PACKAGE_NAME, VENDING_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1555,7 +1557,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( GOOGLE_DIALER_PACKAGE_NAME, GOOGLE_DIALER_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1576,7 +1578,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( GOOGLE_MAPS_PACKAGE_NAME, GOOGLE_MAPS_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -1615,6 +1617,54 @@ public class VirtualDeviceManagerServiceTest { assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); } + @Test + public void canActivityBeLaunched_permissionDialog_flagDisabled_isBlocked() { + mSetFlagsRule.disableFlags(Flags.FLAG_STREAM_PERMISSIONS); + VirtualDeviceParams params = new VirtualDeviceParams.Builder().build(); + mDeviceImpl.close(); + mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params); + doNothing().when(mContext).startActivityAsUser(any(), any(), any()); + + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest( + DISPLAY_ID_1); + ComponentName permissionComponent = getPermissionDialogComponent(); + ActivityInfo activityInfo = getActivityInfo( + permissionComponent.getPackageName(), + permissionComponent.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false)) + .isFalse(); + + Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent( + activityInfo, mAssociationInfo.getDisplayName()); + verify(mContext).startActivityAsUser(argThat(intent -> + intent.filterEquals(blockedAppIntent)), any(), any()); + } + + @Test + public void canActivityBeLaunched_permissionDialog_flagEnabled_isStreamed() { + mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS); + VirtualDeviceParams params = new VirtualDeviceParams.Builder().build(); + mDeviceImpl.close(); + mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params); + + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest( + DISPLAY_ID_1); + ComponentName permissionComponent = getPermissionDialogComponent(); + ActivityInfo activityInfo = getActivityInfo( + permissionComponent.getPackageName(), + permissionComponent.getClassName(), + /* displayOnRemoteDevices */ true, + /* targetDisplayCategory */ null); + assertThat(gwpc.canActivityBeLaunched(activityInfo, null, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false)) + .isTrue(); + } + @Test public void canActivityBeLaunched_activityCanLaunch() { Intent intent = new Intent(ACTION_VIEW, Uri.parse(TEST_SITE)); @@ -1624,7 +1674,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false)) @@ -1648,7 +1698,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW); @@ -1691,7 +1741,7 @@ public class VirtualDeviceManagerServiceTest { ActivityInfo activityInfo = getActivityInfo( NONBLOCKED_APP_PACKAGE_NAME, NONBLOCKED_APP_PACKAGE_NAME, - /* displayOnRemoveDevices */ true, + /* displayOnRemoteDevices */ true, /* targetDisplayCategory */ null); IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW); @@ -1812,6 +1862,13 @@ public class VirtualDeviceManagerServiceTest { NONBLOCKED_APP_PACKAGE_NAME); } + private ComponentName getPermissionDialogComponent() { + Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS); + PackageManager packageManager = mContext.getPackageManager(); + intent.setPackage(packageManager.getPermissionControllerPackageName()); + return intent.resolveActivity(packageManager); + } + /** Helper class to drop permissions temporarily and restore them at the end of a test. */ static final class DropShellPermissionsTemporarily implements AutoCloseable { DropShellPermissionsTemporarily() { diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java index 90d9452fb9c32789e0afb694de9fee691a981cf5..07dd59d2e2d807b402905464a4d4826673c11f1a 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java @@ -32,11 +32,12 @@ import android.companion.virtual.VirtualDevice; import android.companion.virtual.flags.Flags; import android.os.Parcel; import android.platform.test.annotations.Presubmit; -import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -51,6 +52,9 @@ public class VirtualDeviceTest { private static final String DEVICE_NAME = "VirtualDeviceName"; private static final String DISPLAY_NAME = "DisplayName"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private IVirtualDevice mVirtualDevice; @@ -101,9 +105,10 @@ public class VirtualDeviceTest { assertThat(device.getDisplayName().toString()).isEqualTo(DISPLAY_NAME); } - @RequiresFlagsEnabled(Flags.FLAG_VDM_PUBLIC_APIS) @Test public void virtualDevice_getDisplayIds() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + VirtualDevice virtualDevice = new VirtualDevice( mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); @@ -116,9 +121,10 @@ public class VirtualDeviceTest { assertThat(virtualDevice.getDisplayIds()).isEqualTo(displayIds); } - @RequiresFlagsEnabled(Flags.FLAG_VDM_PUBLIC_APIS) @Test public void virtualDevice_hasCustomSensorSupport() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + VirtualDevice virtualDevice = new VirtualDevice( mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index bfa279d5c3d5245a962e66335eb5b2d86201c6cd..2bf13857e537087e7e7280b07c671b3402762fd0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -212,7 +212,30 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mController.dispatchPendingEvents(); assertTaskFragmentParentInfoChangedTransaction(mTask); - assertTaskFragmentAppearedTransaction(); + assertTaskFragmentAppearedTransaction(false /* hasSurfaceControl */); + } + + @Test + public void testOnTaskFragmentAppeared_systemOrganizer() { + mController.unregisterOrganizer(mIOrganizer); + mController.registerOrganizerInternal(mIOrganizer, true /* isSystemOrganizer */); + + // No-op when the TaskFragment is not attached. + mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); + mController.dispatchPendingEvents(); + + verify(mOrganizer, never()).onTransactionReady(any()); + + // Send callback when the TaskFragment is attached. + setupMockParent(mTaskFragment, mTask); + + mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); + mController.dispatchPendingEvents(); + + assertTaskFragmentParentInfoChangedTransaction(mTask); + + // System organizer should receive the SurfaceControl + assertTaskFragmentAppearedTransaction(true /* hasSurfaceControl */); } @Test @@ -1664,7 +1687,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } /** Asserts that there will be a transaction for TaskFragment appeared. */ - private void assertTaskFragmentAppearedTransaction() { + private void assertTaskFragmentAppearedTransaction(boolean hasSurfaceControl) { verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); @@ -1675,6 +1698,11 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertEquals(TYPE_TASK_FRAGMENT_APPEARED, change.getType()); assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); assertEquals(mFragmentToken, change.getTaskFragmentToken()); + if (hasSurfaceControl) { + assertNotNull(change.getTaskFragmentSurfaceControl()); + } else { + assertNull(change.getTaskFragmentSurfaceControl()); + } } /** Asserts that there will be a transaction for TaskFragment info changed. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 6a9bb6c85c705d19935f1b3cc85d1928270160ed..5205bb0038c0cdce0eb76fd49f07542f35a0b903 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -27,8 +27,12 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -123,6 +127,17 @@ public class TaskFragmentTest extends WindowTestsBase { assertEquals(parentSw, mTaskFragment.getConfiguration().smallestScreenWidthDp); } + @Test + public void testUpdateOrganizedTaskFragmentSurface_noSurfaceUpdateWhenOrganizedBySystem() { + clearInvocations(mTransaction); + mTaskFragment.mIsSurfaceManagedBySystemOrganizer = true; + + mTaskFragment.updateOrganizedTaskFragmentSurface(); + + verify(mTransaction, never()).setPosition(eq(mLeash), anyFloat(), anyFloat()); + verify(mTransaction, never()).setWindowCrop(eq(mLeash), anyInt(), anyInt()); + } + @Test public void testShouldStartChangeTransition_relativePositionChange() { final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 36a8fc1c43a0db6b44d1137f95247ffa4658b7a7..a5e0638fec9521faeae4112ab88b0f2e498deab0 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -18,6 +18,7 @@ package android.telephony; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -52,6 +53,7 @@ import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.feature.RcsFeature; import com.android.internal.telephony.ICarrierConfigLoader; +import com.android.internal.telephony.flags.Flags; import com.android.telephony.Rlog; import java.util.List; @@ -9436,22 +9438,9 @@ public class CarrierConfigManager { * </carrier_config> * }</pre> * <p> - * If this carrier config is not present, the device overlay config - * {@code config_satellite_services_supported_by_providers} will be used. If the carrier config - * is present, the supported services associated with the PLMNs listed in the carrier config - * will override that of the device overlay config. The supported satellite services will be - * identified as follows: - * <ul> - * <li>For each PLMN that exists only in the carrier provided satellite services, use the - * carrier provided services as the supported services.</li> - * <li>For each PLMN that is present only in the device provided satellite services, use the - * device provided services as the supported services.</li> - * <li>For each PLMN that is present in both the carrier provided and device provided satellite - * services, use the carrier provided services as the supported services.</li> - * </ul> - * <p> * This config is empty by default. */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE = "carrier_supported_satellite_services_per_provider_bundle"; @@ -9462,9 +9451,8 @@ public class CarrierConfigManager { * satellite provider and the carrier before enabling this flag. * * The default value is false. - * - * @hide */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; @@ -10430,8 +10418,12 @@ public class CarrierConfigManager { sDefaults.putAll(Iwlan.getDefaults()); sDefaults.putStringArray(KEY_CARRIER_CERTIFICATE_STRING_ARRAY, new String[0]); sDefaults.putBoolean(KEY_FORMAT_INCOMING_NUMBER_TO_NATIONAL_FOR_JP_BOOL, false); - sDefaults.putIntArray(KEY_DISCONNECT_CAUSE_PLAY_BUSYTONE_INT_ARRAY, - new int[] {4 /* BUSY */}); + if (Flags.doNotOverridePreciseLabel()) { + sDefaults.putIntArray(KEY_DISCONNECT_CAUSE_PLAY_BUSYTONE_INT_ARRAY, new int[]{}); + } else { + sDefaults.putIntArray(KEY_DISCONNECT_CAUSE_PLAY_BUSYTONE_INT_ARRAY, + new int[]{4 /* BUSY */}); + } sDefaults.putBoolean(KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL, false); sDefaults.putLong(KEY_DATA_SWITCH_VALIDATION_TIMEOUT_LONG, 5000); sDefaults.putStringArray(KEY_MMI_TWO_DIGIT_NUMBER_PATTERN_STRING_ARRAY, new String[0]); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index a4527f12a19955aee56d0f21003fb028e66bea3f..8dc2de8178c432b251e24d0d451b7d5b56940790 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -84,30 +84,12 @@ public final class SatelliteManager { */ @Nullable private final Context mContext; - /** - * Create a new SatelliteManager object pinned to the given subscription ID. - * This is needed only to handle carrier specific satellite features. - * For eg: requestSatelliteAttachEnabledForCarrier and - * requestIsSatelliteAttachEnabledForCarrier - * - * @return a SatelliteManager that uses the given subId for all satellite activities. - * @throws IllegalArgumentException if the subscription is invalid. - * @hide - */ - public SatelliteManager createForSubscriptionId(int subId) { - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - throw new IllegalArgumentException("Invalid subscription ID"); - } - return new SatelliteManager(mContext, subId); - } - /** * Create an instance of the SatelliteManager. * * @param context The context the SatelliteManager belongs to. * @hide */ - public SatelliteManager(@Nullable Context context) { this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } @@ -846,8 +828,8 @@ public final class SatelliteManager { /** * Satellite communication restricted by geolocation. This can be * triggered based upon geofence input provided by carrier to enable or disable satellite. - * @hide */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; /** @hide */ @@ -1643,26 +1625,28 @@ public final class SatelliteManager { * {@code true}.</li> * </ul> * + * @param subId The subscription ID of the carrier. * @param enableSatellite {@code true} to enable the satellite and {@code false} to disable. * @param executor The executor on which the error code listener will be called. * @param resultListener Listener for the {@link SatelliteError} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @hide + * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) - public void requestSatelliteAttachEnabledForCarrier(boolean enableSatellite, + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public void requestSatelliteAttachEnabledForCarrier(int subId, boolean enableSatellite, @NonNull @CallbackExecutor Executor executor, @SatelliteResult @NonNull Consumer<Integer> resultListener) { Objects.requireNonNull(executor); Objects.requireNonNull(resultListener); if (enableSatellite) { - removeSatelliteAttachRestrictionForCarrier( + removeSatelliteAttachRestrictionForCarrier(subId, SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER, executor, resultListener); } else { - addSatelliteAttachRestrictionForCarrier( + addSatelliteAttachRestrictionForCarrier(subId, SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER, executor, resultListener); } } @@ -1671,6 +1655,7 @@ public final class SatelliteManager { * Request to get whether the carrier supported satellite plmn scan and attach by modem is * enabled by user. * + * @param subId The subscription ID of the carrier. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. * If the request is successful, {@link OutcomeReceiver#onResult(Object)} @@ -1681,16 +1666,17 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @hide + * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) - public void requestIsSatelliteAttachEnabledForCarrier( + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public void requestIsSatelliteAttachEnabledForCarrier(int subId, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); - Set<Integer> restrictionReason = getSatelliteAttachRestrictionReasonsForCarrier(); + Set<Integer> restrictionReason = getSatelliteAttachRestrictionReasonsForCarrier(subId); executor.execute(() -> callback.onResult( !restrictionReason.contains(SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER))); } @@ -1699,19 +1685,25 @@ public final class SatelliteManager { * Add a restriction reason for disallowing carrier supported satellite plmn scan and attach * by modem. * + * @param subId The subscription ID of the carrier. * @param reason Reason for disallowing satellite communication. * @param executor The executor on which the error code listener will be called. * @param resultListener Listener for the {@link SatelliteError} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @hide + * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) - public void addSatelliteAttachRestrictionForCarrier( + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public void addSatelliteAttachRestrictionForCarrier(int subId, @SatelliteCommunicationRestrictionReason int reason, @NonNull @CallbackExecutor Executor executor, @SatelliteResult @NonNull Consumer<Integer> resultListener) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + throw new IllegalArgumentException("Invalid subscription ID"); + } + try { ITelephony telephony = getITelephony(); if (telephony != null) { @@ -1722,7 +1714,7 @@ public final class SatelliteManager { () -> resultListener.accept(result))); } }; - telephony.addSatelliteAttachRestrictionForCarrier(mSubId, reason, errorCallback); + telephony.addSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { throw new IllegalStateException("telephony service is null."); } @@ -1736,19 +1728,25 @@ public final class SatelliteManager { * Remove a restriction reason for disallowing carrier supported satellite plmn scan and attach * by modem. * + * @param subId The subscription ID of the carrier. * @param reason Reason for disallowing satellite communication. * @param executor The executor on which the error code listener will be called. * @param resultListener Listener for the {@link SatelliteError} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @hide + * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) - public void removeSatelliteAttachRestrictionForCarrier( + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public void removeSatelliteAttachRestrictionForCarrier(int subId, @SatelliteCommunicationRestrictionReason int reason, @NonNull @CallbackExecutor Executor executor, @SatelliteResult @NonNull Consumer<Integer> resultListener) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + throw new IllegalArgumentException("Invalid subscription ID"); + } + try { ITelephony telephony = getITelephony(); if (telephony != null) { @@ -1759,7 +1757,7 @@ public final class SatelliteManager { () -> resultListener.accept(result))); } }; - telephony.removeSatelliteAttachRestrictionForCarrier(mSubId, reason, errorCallback); + telephony.removeSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { throw new IllegalStateException("telephony service is null."); } @@ -1773,34 +1771,40 @@ public final class SatelliteManager { * Get reasons for disallowing satellite attach, as requested by * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)} * + * @param subId The subscription ID of the carrier. * @return Set of reasons for disallowing satellite communication. * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @hide + * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @SatelliteCommunicationRestrictionReason - public @NonNull Set<Integer> getSatelliteAttachRestrictionReasonsForCarrier() { + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @NonNull public Set<Integer> getSatelliteAttachRestrictionReasonsForCarrier(int subId) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + throw new IllegalArgumentException("Invalid subscription ID"); + } + try { ITelephony telephony = getITelephony(); if (telephony != null) { int[] receivedArray = - telephony.getSatelliteAttachRestrictionReasonsForCarrier(mSubId); + telephony.getSatelliteAttachRestrictionReasonsForCarrier(subId); if (receivedArray.length == 0) { - logd("received set is empty, create empty set"); + logd("receivedArray is empty, create empty set"); return new HashSet<>(); } else { return Arrays.stream(receivedArray).boxed().collect(Collectors.toSet()); } } else { - throw new IllegalStateException("telephony service is null."); + throw new IllegalStateException("Telephony service is null."); } } catch (RemoteException ex) { loge("getSatelliteAttachRestrictionReasonsForCarrier() RemoteException: " + ex); ex.rethrowFromSystemServer(); } - return null; + return new HashSet<>(); } private static ITelephony getITelephony() { diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index 02661de34fb26d530d3c5268178f0e371cb0c143..0fcd0d622d365f1ca333d21b45d1b65dbc94faef 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -392,7 +392,11 @@ oneway interface ISatellite { * * @param simSlot Indicates the SIM slot to which this API will be applied. The modem will use * this information to determine the relevant carrier. - * @param plmnList The list of roaming PLMN used for connecting to satellite networks. + * @param carrierPlmnList The list of roaming PLMN used for connecting to satellite networks + * supported by user subscription. + * @param allSatellitePlmnList Modem should use the allSatellitePlmnList to identify satellite + * PLMNs that are not supported by the carrier and make sure not to + * attach to them. * @param resultCallback The callback to receive the error code result of the operation. * * Valid error codes returned: @@ -404,8 +408,8 @@ oneway interface ISatellite { * SatelliteError:RADIO_NOT_AVAILABLE * SatelliteError:REQUEST_NOT_SUPPORTED */ - void setSatellitePlmn(int simSlot, in List<String> plmnList, - in IIntegerConsumer resultCallback); + void setSatellitePlmn(int simSlot, in List<String> carrierPlmnList, + in List<String> allSatellitePlmnList, in IIntegerConsumer resultCallback); /** * Enable or disable satellite in the cellular modem associated with a carrier. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index 6451daf2e752bdff9631b1dcb09bf7487436e71c..a9c09c94d57c5c8362e502f787f99096ab34fb1b 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -214,12 +214,12 @@ public class SatelliteImplBase extends SatelliteService { } @Override - public void setSatellitePlmn(int simSlot, List<String> plmnList, - IIntegerConsumer errorCallback) + public void setSatellitePlmn(int simSlot, List<String> carrierPlmnList, + List<String> devicePlmnList, IIntegerConsumer errorCallback) throws RemoteException { executeMethodAsync( - () -> SatelliteImplBase.this - .setSatellitePlmn(simSlot, plmnList, errorCallback), + () -> SatelliteImplBase.this.setSatellitePlmn( + simSlot, carrierPlmnList, devicePlmnList, errorCallback), "setSatellitePlmn"); } @@ -655,13 +655,15 @@ public class SatelliteImplBase extends SatelliteService { * SIM profile. Acquisition of satellite based system is lower priority to terrestrial * networks. UE shall make all attempts to acquire terrestrial service prior to camping on * satellite LTE service. - * This method must only take effect if {@link #setSatelliteEnabledForCarrier} is {@code true}, - * and return an error otherwise. * * @param simLogicalSlotIndex Indicates the SIM logical slot index to which this API will be * applied. The modem will use this information to determine the relevant carrier. * @param errorCallback The callback to receive the error code result of the operation. - * @param plmnList The list of roaming PLMN used for connecting to satellite networks. + * @param carrierPlmnList The list of roaming PLMN used for connecting to satellite networks + * supported by user subscription. + * @param allSatellitePlmnList Modem should use the allSatellitePlmnList to identify satellite + * PLMNs that are not supported by the carrier and make sure not to + * attach to them. * * Valid error codes returned: * SatelliteError:NONE @@ -672,7 +674,8 @@ public class SatelliteImplBase extends SatelliteService { * SatelliteError:RADIO_NOT_AVAILABLE * SatelliteError:REQUEST_NOT_SUPPORTED */ - public void setSatellitePlmn(@NonNull int simLogicalSlotIndex, @NonNull List<String> plmnList, + public void setSatellitePlmn(@NonNull int simLogicalSlotIndex, + @NonNull List<String> carrierPlmnList, @NonNull List<String> allSatellitePlmnList, @NonNull IIntegerConsumer errorCallback) { // stub implementation } diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java index 95cbff9eeb1235a9a0b94b0324524c69eacec24a..d176b5e97b0b96b94339215cf6ab1dc9d209a1ed 100644 --- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java +++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java @@ -69,7 +69,8 @@ public class HostTestUtils { String methodDescriptor, String callbackMethod ) { - callStaticMethodByName(callbackMethod, methodClass, methodName, methodDescriptor); + callStaticMethodByName(callbackMethod, "method call hook", methodClass, + methodName, methodDescriptor); } /** @@ -167,31 +168,38 @@ public class HostTestUtils { logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName() + " calling hook " + callbackMethod); - callStaticMethodByName(callbackMethod, loadedClass); + callStaticMethodByName(callbackMethod, "class load hook", loadedClass); } - private static void callStaticMethodByName(String classAndMethodName, Object... args) { + private static void callStaticMethodByName(String classAndMethodName, + String description, Object... args) { // Forward the call to callbackMethod. final int lastPeriod = classAndMethodName.lastIndexOf("."); - final String className = classAndMethodName.substring(0, lastPeriod); - final String methodName = classAndMethodName.substring(lastPeriod + 1); - if (lastPeriod < 0 || className.isEmpty() || methodName.isEmpty()) { + if ((lastPeriod) < 0 || (lastPeriod == classAndMethodName.length() - 1)) { throw new HostTestException(String.format( - "Unable to find class load hook: malformed method name \"%s\"", + "Unable to find %s: malformed method name \"%s\"", + description, classAndMethodName)); } + final String className = classAndMethodName.substring(0, lastPeriod); + final String methodName = classAndMethodName.substring(lastPeriod + 1); + Class<?> clazz = null; try { clazz = Class.forName(className); } catch (Exception e) { throw new HostTestException(String.format( - "Unable to find class load hook: Class %s not found", className), e); + "Unable to find %s: Class %s not found", + description, + className), e); } if (!Modifier.isPublic(clazz.getModifiers())) { throw new HostTestException(String.format( - "Unable to find class load hook: Class %s must be public", className)); + "Unable to find %s: Class %s must be public", + description, + className)); } Class<?>[] argTypes = new Class[args.length]; @@ -204,25 +212,23 @@ public class HostTestUtils { method = clazz.getMethod(methodName, argTypes); } catch (Exception e) { throw new HostTestException(String.format( - "Unable to find class load hook: class %s doesn't have method %s" + "Unable to find %s: class %s doesn't have method %s" + " (method must take exactly one parameter of type Class," + " and public static)", - className, - methodName), e); + description, className, methodName), e); } if (!(Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers()))) { throw new HostTestException(String.format( - "Unable to find class load hook: Method %s in class %s must be public static", - methodName, className)); + "Unable to find %s: Method %s in class %s must be public static", + description, methodName, className)); } try { method.invoke(null, args); } catch (Exception e) { throw new HostTestException(String.format( - "Unable to invoke class load hook %s.%s", - className, - methodName), e); + "Unable to invoke %s %s.%s", + description, className, methodName), e); } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt index 7d7852ab162e9d760b34b410fc113a448839b23b..f75062b3a878d82d14b1ca827fb416b6da5bc5d4 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt @@ -32,6 +32,10 @@ fun normalizeTextLine(s: String): String { return uncommented.trim() } +/** + * Concatenate list [a] and [b] and return it. As an optimization, it returns an input + * [List] as-is if the other [List] is empty, so do not modify input [List]'s. + */ fun <T> addLists(a: List<T>, b: List<T>): List<T> { if (a.isEmpty()) { return b @@ -42,6 +46,10 @@ fun <T> addLists(a: List<T>, b: List<T>): List<T> { return a + b } +/** + * Add element [b] to list [a] if [b] is not null. Otherwise, just return [a]. + * (because the method may return [a] as-is, do not modify it after passing it.) + */ fun <T> addNonNullElement(a: List<T>, b: T?): List<T> { if (b == null) { return a diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt index cbbf91b49dedaade19a8426a20f2690150ce176f..758de4dfccf820d12a98c572063d0adae55a918f 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -131,7 +131,7 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { priority = 6, severity = Severity.ERROR, implementation = Implementation( - EnforcePermissionDetector::class.java, + EnforcePermissionHelperDetector::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) ) )