diff options
146 files changed, 5198 insertions, 1487 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index b42face946a8..19b476a5c5a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -143,6 +143,7 @@ package android { field public static final String READ_MEDIA_AUDIO = "android.permission.READ_MEDIA_AUDIO"; field public static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES"; field public static final String READ_MEDIA_VIDEO = "android.permission.READ_MEDIA_VIDEO"; + field public static final String READ_MEDIA_VISUAL_USER_SELECTED = "android.permission.READ_MEDIA_VISUAL_USER_SELECTED"; field public static final String READ_NEARBY_STREAMING_POLICY = "android.permission.READ_NEARBY_STREAMING_POLICY"; field public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS"; field public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; @@ -8955,9 +8956,18 @@ package android.appwidget { package android.companion { + public final class AssociatedDevice implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.bluetooth.le.ScanResult getBleDevice(); + method @Nullable public android.bluetooth.BluetoothDevice getBluetoothDevice(); + method @Nullable public android.net.wifi.ScanResult getWifiDevice(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.companion.AssociatedDevice> CREATOR; + } + public final class AssociationInfo implements android.os.Parcelable { method public int describeContents(); - method @Nullable public android.os.Parcelable getAssociatedDevice(); + method @Nullable public android.companion.AssociatedDevice getAssociatedDevice(); method @Nullable public android.net.MacAddress getDeviceMacAddress(); method @Nullable public String getDeviceProfile(); method @Nullable public CharSequence getDisplayName(); @@ -41762,6 +41772,7 @@ package android.telephony { field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool"; field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool"; field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array"; + field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long"; @@ -44069,6 +44080,7 @@ package android.telephony { field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5 + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index dcbe7ed13408..352b4f98edd3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -587,6 +587,7 @@ package android.app { field public static final String OPSTR_READ_MEDIA_AUDIO = "android:read_media_audio"; field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images"; field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video"; + field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected"; field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio"; field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast"; field public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio"; @@ -15924,10 +15925,17 @@ package android.view { package android.view.accessibility { + public abstract class AccessibilityDisplayProxy { + ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>); + method public int getDisplayId(); + } + public final class AccessibilityManager { method public int getAccessibilityWindowId(@Nullable android.os.IBinder); method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void performAccessibilityShortcut(); + method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean registerDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy); method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void registerSystemAction(@NonNull android.app.RemoteAction, int); + method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean unregisterDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy); method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void unregisterSystemAction(int); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 9c1a55b84ca5..cdb1510b388e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3157,7 +3157,7 @@ package android.view.inputmethod { } public final class InputMethodManager { - method public void addVirtualStylusIdForTestSession(); + method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession(); method public int getDisplayId(); method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int); method public boolean hasActiveInputConnection(@Nullable android.view.View); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 1b972e0cb81a..267e5b699241 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1360,9 +1360,17 @@ public class AppOpsManager { */ public static final int OP_RUN_LONG_JOBS = AppProtoEnums.APP_OP_RUN_LONG_JOBS; + /** + * Notify apps that they have been granted URI permission photos + * + * @hide + */ + public static final int OP_READ_MEDIA_VISUAL_USER_SELECTED = + AppProtoEnums.APP_OP_READ_MEDIA_VISUAL_USER_SELECTED; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 123; + public static final int _NUM_OP = 124; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -1833,6 +1841,14 @@ public class AppOpsManager { @SystemApi public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio"; + /** + * Notify apps that they have been granted URI permission photos + * + * @hide + */ + @SystemApi + public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = + "android:read_media_visual_user_selected"; /** * Record audio from near-field microphone (ie. TV remote) @@ -1948,6 +1964,7 @@ public class AppOpsManager { OP_MANAGE_MEDIA, OP_TURN_SCREEN_ON, OP_RUN_LONG_JOBS, + OP_READ_MEDIA_VISUAL_USER_SELECTED, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2329,7 +2346,11 @@ public class AppOpsManager { "RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode( AppOpsManager.MODE_ALLOWED).build(), new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS") - .setPermission(Manifest.permission.RUN_LONG_JOBS).build() + .setPermission(Manifest.permission.RUN_LONG_JOBS).build(), + new AppOpInfo.Builder(OP_READ_MEDIA_VISUAL_USER_SELECTED, + OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, "READ_MEDIA_VISUAL_USER_SELECTED") + .setPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) + .setDefaultMode(AppOpsManager.MODE_ALLOWED).build() }; /** diff --git a/core/java/android/companion/AssociatedDevice.java b/core/java/android/companion/AssociatedDevice.java index 3758cdb680b1..a8336615fde5 100644 --- a/core/java/android/companion/AssociatedDevice.java +++ b/core/java/android/companion/AssociatedDevice.java @@ -16,6 +16,7 @@ package android.companion; +import android.bluetooth.BluetoothDevice; import android.os.Parcel; import android.os.Parcelable; @@ -23,19 +24,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** - * Loose wrapper around device parcelable. Device can be one of three types: + * Container for device info from an association that is not self-managed. + * Device can be one of three types: * * <ul> * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li> * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li> * <li>for WiFi - {@link android.net.wifi.ScanResult}</li> * </ul> - * - * This class serves as temporary wrapper to deliver a loosely-typed parcelable object from - * {@link com.android.companiondevicemanager.CompanionDeviceActivity} to the Companion app, - * and should only be used internally. - * - * @hide */ public final class AssociatedDevice implements Parcelable { private static final int CLASSIC_BLUETOOTH = 0; @@ -44,6 +40,7 @@ public final class AssociatedDevice implements Parcelable { @NonNull private final Parcelable mDevice; + /** @hide */ public AssociatedDevice(@NonNull Parcelable device) { mDevice = device; } @@ -54,11 +51,39 @@ public final class AssociatedDevice implements Parcelable { } /** - * Return device info. Cast to expected device type. + * Return bluetooth device info. Null if associated device is not a bluetooth device. + * @return Remote bluetooth device details containing MAC address. */ - @NonNull - public Parcelable getDevice() { - return mDevice; + @Nullable + public BluetoothDevice getBluetoothDevice() { + if (mDevice instanceof BluetoothDevice) { + return (BluetoothDevice) mDevice; + } + return null; + } + + /** + * Return bluetooth LE device info. Null if associated device is not a BLE device. + * @return BLE scan result containing details of detected BLE device. + */ + @Nullable + public android.bluetooth.le.ScanResult getBleDevice() { + if (mDevice instanceof android.bluetooth.le.ScanResult) { + return (android.bluetooth.le.ScanResult) mDevice; + } + return null; + } + + /** + * Return Wi-Fi device info. Null if associated device is not a Wi-Fi device. + * @return Wi-Fi scan result containing details of detected access point. + */ + @Nullable + public android.net.wifi.ScanResult getWifiDevice() { + if (mDevice instanceof android.net.wifi.ScanResult) { + return (android.net.wifi.ScanResult) mDevice; + } + return null; } @Override diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 93964b3f4180..5fd39feceb23 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -164,20 +164,19 @@ public final class AssociationInfo implements Parcelable { /** * Companion device that was associated. Note that this field is not persisted across sessions. - * - * Cast to expected device type before use: + * Device can be one of the following types: * * <ul> - * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li> - * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li> - * <li>for WiFi - {@link android.net.wifi.ScanResult}</li> + * <li>for classic Bluetooth - {@link AssociatedDevice#getBluetoothDevice()}</li> + * <li>for Bluetooth LE - {@link AssociatedDevice#getBleDevice()}</li> + * <li>for WiFi - {@link AssociatedDevice#getWifiDevice()}</li> * </ul> * * @return the companion device that was associated, or {@code null} if the device is - * self-managed. + * self-managed or this association info was retrieved from persistent storage. */ - public @Nullable Parcelable getAssociatedDevice() { - return mAssociatedDevice == null ? null : mAssociatedDevice.getDevice(); + public @Nullable AssociatedDevice getAssociatedDevice() { + return mAssociatedDevice; } /** diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java new file mode 100644 index 000000000000..9cc9c7289f9b --- /dev/null +++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java @@ -0,0 +1,164 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Per-provider metadata and entries for the create-credential flow. + * + * @hide + */ +public class CreateCredentialProviderData extends ProviderData implements Parcelable { + @NonNull + private final List<Entry> mSaveEntries; + @NonNull + private final List<Entry> mActionChips; + private final boolean mIsDefaultProvider; + @Nullable + private final Entry mRemoteEntry; + + public CreateCredentialProviderData( + @NonNull String providerFlattenedComponentName, @NonNull List<Entry> saveEntries, + @NonNull List<Entry> actionChips, boolean isDefaultProvider, + @Nullable Entry remoteEntry) { + super(providerFlattenedComponentName); + mSaveEntries = saveEntries; + mActionChips = actionChips; + mIsDefaultProvider = isDefaultProvider; + mRemoteEntry = remoteEntry; + } + + @NonNull + public List<Entry> getSaveEntries() { + return mSaveEntries; + } + + @NonNull + public List<Entry> getActionChips() { + return mActionChips; + } + + public boolean isDefaultProvider() { + return mIsDefaultProvider; + } + + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + protected CreateCredentialProviderData(@NonNull Parcel in) { + super(in); + + List<Entry> credentialEntries = new ArrayList<>(); + in.readTypedList(credentialEntries, Entry.CREATOR); + mSaveEntries = credentialEntries; + AnnotationValidations.validate(NonNull.class, null, mSaveEntries); + + List<Entry> actionChips = new ArrayList<>(); + in.readTypedList(actionChips, Entry.CREATOR); + mActionChips = actionChips; + AnnotationValidations.validate(NonNull.class, null, mActionChips); + + mIsDefaultProvider = in.readBoolean(); + + Entry remoteEntry = in.readTypedObject(Entry.CREATOR); + mRemoteEntry = remoteEntry; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeTypedList(mSaveEntries); + dest.writeTypedList(mActionChips); + dest.writeBoolean(isDefaultProvider()); + dest.writeTypedObject(mRemoteEntry, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<CreateCredentialProviderData> CREATOR = + new Creator<CreateCredentialProviderData>() { + @Override + public CreateCredentialProviderData createFromParcel(@NonNull Parcel in) { + return new CreateCredentialProviderData(in); + } + + @Override + public CreateCredentialProviderData[] newArray(int size) { + return new CreateCredentialProviderData[size]; + } + }; + + /** + * Builder for {@link CreateCredentialProviderData}. + * + * @hide + */ + public static class Builder { + private @NonNull String mProviderFlattenedComponentName; + private @NonNull List<Entry> mSaveEntries = new ArrayList<>(); + private @NonNull List<Entry> mActionChips = new ArrayList<>(); + private boolean mIsDefaultProvider = false; + private @Nullable Entry mRemoteEntry = null; + + /** Constructor with required properties. */ + public Builder(@NonNull String providerFlattenedComponentName) { + mProviderFlattenedComponentName = providerFlattenedComponentName; + } + + /** Sets the list of save credential entries to be displayed to the user. */ + @NonNull + public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) { + mSaveEntries = credentialEntries; + return this; + } + + /** Sets the list of action chips to be displayed to the user. */ + @NonNull + public Builder setActionChips(@NonNull List<Entry> actionChips) { + mActionChips = actionChips; + return this; + } + + /** Sets whether this provider is the user's selected default provider. */ + @NonNull + public Builder setIsDefaultProvider(boolean isDefaultProvider) { + mIsDefaultProvider = isDefaultProvider; + return this; + } + + /** Builds a {@link CreateCredentialProviderData}. */ + @NonNull + public CreateCredentialProviderData build() { + return new CreateCredentialProviderData(mProviderFlattenedComponentName, + mSaveEntries, mActionChips, mIsDefaultProvider, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java new file mode 100644 index 000000000000..73c8dbe427a7 --- /dev/null +++ b/core/java/android/credentials/ui/DisabledProviderData.java @@ -0,0 +1,60 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Metadata of a disabled provider. + * + * @hide + */ +public class DisabledProviderData extends ProviderData implements Parcelable { + + public DisabledProviderData( + @NonNull String providerFlattenedComponentName) { + super(providerFlattenedComponentName); + } + + protected DisabledProviderData(@NonNull Parcel in) { + super(in); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<DisabledProviderData> CREATOR = new Creator<>() { + @Override + public DisabledProviderData createFromParcel(@NonNull Parcel in) { + return new DisabledProviderData(in); + } + + @Override + public DisabledProviderData[] newArray(int size) { + return new DisabledProviderData[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java new file mode 100644 index 000000000000..834f9825208f --- /dev/null +++ b/core/java/android/credentials/ui/GetCredentialProviderData.java @@ -0,0 +1,174 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Per-provider metadata and entries for the get-credential flow. + * + * @hide + */ +public class GetCredentialProviderData extends ProviderData implements Parcelable { + @NonNull + private final List<Entry> mCredentialEntries; + @NonNull + private final List<Entry> mActionChips; + @Nullable + private final Entry mAuthenticationEntry; + @Nullable + private final Entry mRemoteEntry; + + public GetCredentialProviderData( + @NonNull String providerFlattenedComponentName, @NonNull List<Entry> credentialEntries, + @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry, + @Nullable Entry remoteEntry) { + super(providerFlattenedComponentName); + mCredentialEntries = credentialEntries; + mActionChips = actionChips; + mAuthenticationEntry = authenticationEntry; + mRemoteEntry = remoteEntry; + } + + @NonNull + public List<Entry> getCredentialEntries() { + return mCredentialEntries; + } + + @NonNull + public List<Entry> getActionChips() { + return mActionChips; + } + + @Nullable + public Entry getAuthenticationEntry() { + return mAuthenticationEntry; + } + + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + protected GetCredentialProviderData(@NonNull Parcel in) { + super(in); + + List<Entry> credentialEntries = new ArrayList<>(); + in.readTypedList(credentialEntries, Entry.CREATOR); + mCredentialEntries = credentialEntries; + AnnotationValidations.validate(NonNull.class, null, mCredentialEntries); + + List<Entry> actionChips = new ArrayList<>(); + in.readTypedList(actionChips, Entry.CREATOR); + mActionChips = actionChips; + AnnotationValidations.validate(NonNull.class, null, mActionChips); + + Entry authenticationEntry = in.readTypedObject(Entry.CREATOR); + mAuthenticationEntry = authenticationEntry; + + Entry remoteEntry = in.readTypedObject(Entry.CREATOR); + mRemoteEntry = remoteEntry; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeTypedList(mCredentialEntries); + dest.writeTypedList(mActionChips); + dest.writeTypedObject(mAuthenticationEntry, flags); + dest.writeTypedObject(mRemoteEntry, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<GetCredentialProviderData> CREATOR = + new Creator<GetCredentialProviderData>() { + @Override + public GetCredentialProviderData createFromParcel(@NonNull Parcel in) { + return new GetCredentialProviderData(in); + } + + @Override + public GetCredentialProviderData[] newArray(int size) { + return new GetCredentialProviderData[size]; + } + }; + + /** + * Builder for {@link GetCredentialProviderData}. + * + * @hide + */ + public static class Builder { + private @NonNull String mProviderFlattenedComponentName; + private @NonNull List<Entry> mCredentialEntries = new ArrayList<>(); + private @NonNull List<Entry> mActionChips = new ArrayList<>(); + private @Nullable Entry mAuthenticationEntry = null; + private @Nullable Entry mRemoteEntry = null; + + /** Constructor with required properties. */ + public Builder(@NonNull String providerFlattenedComponentName) { + mProviderFlattenedComponentName = providerFlattenedComponentName; + } + + /** Sets the list of save / get credential entries to be displayed to the user. */ + @NonNull + public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) { + mCredentialEntries = credentialEntries; + return this; + } + + /** Sets the list of action chips to be displayed to the user. */ + @NonNull + public Builder setActionChips(@NonNull List<Entry> actionChips) { + mActionChips = actionChips; + return this; + } + + /** Sets the authentication entry to be displayed to the user. */ + @NonNull + public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) { + mAuthenticationEntry = authenticationEntry; + return this; + } + + /** Sets the remote entry to be displayed to the user. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + + /** Builds a {@link GetCredentialProviderData}. */ + @NonNull + public GetCredentialProviderData build() { + return new GetCredentialProviderData(mProviderFlattenedComponentName, + mCredentialEntries, mActionChips, mAuthenticationEntry, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java index 1b70ea4ebd71..475169670eb1 100644 --- a/core/java/android/credentials/ui/IntentFactory.java +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -30,15 +30,20 @@ import java.util.ArrayList; */ public class IntentFactory { /** Generate a new launch intent to the . */ - public static Intent newIntent(RequestInfo requestInfo, - ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) { + public static Intent newIntent( + RequestInfo requestInfo, + ArrayList<ProviderData> enabledProviderDataList, + ArrayList<DisabledProviderData> disabledProviderDataList, + ResultReceiver resultReceiver) { Intent intent = new Intent(); // TODO: define these as proper config strings. String activityName = "com.android.credentialmanager/.CredentialSelectorActivity"; intent.setComponent(ComponentName.unflattenFromString(activityName)); intent.putParcelableArrayListExtra( - ProviderData.EXTRA_PROVIDER_DATA_LIST, providerDataList); + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList); + intent.putParcelableArrayListExtra( + ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList); intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo); intent.putExtra(Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver)); diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java index 3728469d723d..eeaeb46e7896 100644 --- a/core/java/android/credentials/ui/ProviderData.java +++ b/core/java/android/credentials/ui/ProviderData.java @@ -16,232 +16,62 @@ package android.credentials.ui; -import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; -import java.util.ArrayList; -import java.util.List; - /** - * Holds metadata and credential entries for a single provider. + * Super class for data structures that hold metadata and credential entries for a single provider. * * @hide */ -public class ProviderData implements Parcelable { +public abstract class ProviderData implements Parcelable { /** - * The intent extra key for the list of {@code ProviderData} when launching the UX - * activities. + * The intent extra key for the list of {@code ProviderData} from active providers when + * launching the UX activities. + */ + public static final String EXTRA_ENABLED_PROVIDER_DATA_LIST = + "android.credentials.ui.extra.ENABLED_PROVIDER_DATA_LIST"; + /** + * The intent extra key for the list of {@code ProviderData} from disabled providers when + * launching the UX activities. */ - public static final String EXTRA_PROVIDER_DATA_LIST = - "android.credentials.ui.extra.PROVIDER_DATA_LIST"; + public static final String EXTRA_DISABLED_PROVIDER_DATA_LIST = + "android.credentials.ui.extra.DISABLED_PROVIDER_DATA_LIST"; @NonNull private final String mProviderFlattenedComponentName; - @NonNull - private final String mProviderDisplayName; - @Nullable - private final Icon mIcon; - @NonNull - private final List<Entry> mCredentialEntries; - @NonNull - private final List<Entry> mActionChips; - @Nullable - private final Entry mAuthenticationEntry; - - private final @CurrentTimeMillisLong long mLastUsedTimeMillis; public ProviderData( - @NonNull String providerFlattenedComponentName, @NonNull String providerDisplayName, - @Nullable Icon icon, @NonNull List<Entry> credentialEntries, - @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry, - @CurrentTimeMillisLong long lastUsedTimeMillis) { + @NonNull String providerFlattenedComponentName) { mProviderFlattenedComponentName = providerFlattenedComponentName; - mProviderDisplayName = providerDisplayName; - mIcon = icon; - mCredentialEntries = credentialEntries; - mActionChips = actionChips; - mAuthenticationEntry = authenticationEntry; - mLastUsedTimeMillis = lastUsedTimeMillis; } - /** Returns the unique provider id. */ + /** + * Returns provider component name. + * It also serves as the unique identifier for this provider. + */ @NonNull public String getProviderFlattenedComponentName() { return mProviderFlattenedComponentName; } - @NonNull - public String getProviderDisplayName() { - return mProviderDisplayName; - } - - @Nullable - public Icon getIcon() { - return mIcon; - } - - @NonNull - public List<Entry> getCredentialEntries() { - return mCredentialEntries; - } - - @NonNull - public List<Entry> getActionChips() { - return mActionChips; - } - - @Nullable - public Entry getAuthenticationEntry() { - return mAuthenticationEntry; - } - - /** Returns the time when the provider was last used. */ - public @CurrentTimeMillisLong long getLastUsedTimeMillis() { - return mLastUsedTimeMillis; - } - protected ProviderData(@NonNull Parcel in) { String providerFlattenedComponentName = in.readString8(); mProviderFlattenedComponentName = providerFlattenedComponentName; AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName); - - String providerDisplayName = in.readString8(); - mProviderDisplayName = providerDisplayName; - AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName); - - Icon icon = in.readTypedObject(Icon.CREATOR); - mIcon = icon; - - List<Entry> credentialEntries = new ArrayList<>(); - in.readTypedList(credentialEntries, Entry.CREATOR); - mCredentialEntries = credentialEntries; - AnnotationValidations.validate(NonNull.class, null, mCredentialEntries); - - List<Entry> actionChips = new ArrayList<>(); - in.readTypedList(actionChips, Entry.CREATOR); - mActionChips = actionChips; - AnnotationValidations.validate(NonNull.class, null, mActionChips); - - Entry authenticationEntry = in.readTypedObject(Entry.CREATOR); - mAuthenticationEntry = authenticationEntry; - - long lastUsedTimeMillis = in.readLong(); - mLastUsedTimeMillis = lastUsedTimeMillis; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mProviderFlattenedComponentName); - dest.writeString8(mProviderDisplayName); - dest.writeTypedObject(mIcon, flags); - dest.writeTypedList(mCredentialEntries); - dest.writeTypedList(mActionChips); - dest.writeTypedObject(mAuthenticationEntry, flags); - dest.writeLong(mLastUsedTimeMillis); } @Override public int describeContents() { return 0; } - - public static final @NonNull Creator<ProviderData> CREATOR = new Creator<ProviderData>() { - @Override - public ProviderData createFromParcel(@NonNull Parcel in) { - return new ProviderData(in); - } - - @Override - public ProviderData[] newArray(int size) { - return new ProviderData[size]; - } - }; - - /** - * Builder for {@link ProviderData}. - * - * @hide - */ - public static class Builder { - private @NonNull String mProviderFlattenedComponentName; - private @NonNull String mProviderDisplayName; - private @Nullable Icon mIcon; - private @NonNull List<Entry> mCredentialEntries = new ArrayList<>(); - private @NonNull List<Entry> mActionChips = new ArrayList<>(); - private @Nullable Entry mAuthenticationEntry = null; - private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L; - - /** Constructor with required properties. */ - public Builder(@NonNull String providerFlattenedComponentName, - @NonNull String providerDisplayName, - @Nullable Icon icon) { - mProviderFlattenedComponentName = providerFlattenedComponentName; - mProviderDisplayName = providerDisplayName; - mIcon = icon; - } - - /** Sets the unique provider id. */ - @NonNull - public Builder setProviderFlattenedComponentName(@NonNull String providerFlattenedComponentName) { - mProviderFlattenedComponentName = providerFlattenedComponentName; - return this; - } - - /** Sets the provider display name to be displayed to the user. */ - @NonNull - public Builder setProviderDisplayName(@NonNull String providerDisplayName) { - mProviderDisplayName = providerDisplayName; - return this; - } - - /** Sets the provider icon to be displayed to the user. */ - @NonNull - public Builder setIcon(@NonNull Icon icon) { - mIcon = icon; - return this; - } - - /** Sets the list of save / get credential entries to be displayed to the user. */ - @NonNull - public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) { - mCredentialEntries = credentialEntries; - return this; - } - - /** Sets the list of action chips to be displayed to the user. */ - @NonNull - public Builder setActionChips(@NonNull List<Entry> actionChips) { - mActionChips = actionChips; - return this; - } - - /** Sets the authentication entry to be displayed to the user. */ - @NonNull - public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) { - mAuthenticationEntry = authenticationEntry; - return this; - } - - /** Sets the time when the provider was last used. */ - @NonNull - public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) { - mLastUsedTimeMillis = lastUsedTimeMillis; - return this; - } - - /** Builds a {@link ProviderData}. */ - @NonNull - public ProviderData build() { - return new ProviderData(mProviderFlattenedComponentName, mProviderDisplayName, - mIcon, mCredentialEntries, - mActionChips, mAuthenticationEntry, mLastUsedTimeMillis); - } - } } diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 619b08ec9ca7..59d511838edc 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -69,6 +69,7 @@ public class RequestInfo implements Parcelable { private final boolean mIsFirstUsage; + // TODO: change to package name @NonNull private final String mAppDisplayName; diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 36ac1a0cb21c..8a9213515122 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -533,7 +533,6 @@ public final class ProgramSelector implements Parcelable { mProgramType = in.readInt(); mPrimaryId = in.readTypedObject(Identifier.CREATOR); mSecondaryIds = in.createTypedArray(Identifier.CREATOR); - Arrays.sort(mSecondaryIds); if (Stream.of(mSecondaryIds).anyMatch(id -> id == null)) { throw new IllegalArgumentException("secondaryIds list must not contain nulls"); } diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index b4010a4eefda..0f7c9b63dac0 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -111,6 +111,12 @@ public final class FillRequest implements Parcelable { */ public static final @RequestFlags int FLAG_IME_SHOWING = 0x80; + /** + * Indicates whether autofill session should reset the fill dialog state. + * @hide + */ + public static final @RequestFlags int FLAG_RESET_FILL_DIALOG_STATE = 0x100; + /** @hide */ public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE; @@ -208,7 +214,8 @@ public final class FillRequest implements Parcelable { FLAG_PASSWORD_INPUT_TYPE, FLAG_VIEW_NOT_FOCUSED, FLAG_SUPPORTS_FILL_DIALOG, - FLAG_IME_SHOWING + FLAG_IME_SHOWING, + FLAG_RESET_FILL_DIALOG_STATE }) @Retention(RetentionPolicy.SOURCE) @DataClass.Generated.Member @@ -236,6 +243,8 @@ public final class FillRequest implements Parcelable { return "FLAG_SUPPORTS_FILL_DIALOG"; case FLAG_IME_SHOWING: return "FLAG_IME_SHOWING"; + case FLAG_RESET_FILL_DIALOG_STATE: + return "FLAG_RESET_FILL_DIALOG_STATE"; default: return Integer.toHexString(value); } } @@ -312,7 +321,8 @@ public final class FillRequest implements Parcelable { | FLAG_PASSWORD_INPUT_TYPE | FLAG_VIEW_NOT_FOCUSED | FLAG_SUPPORTS_FILL_DIALOG - | FLAG_IME_SHOWING); + | FLAG_IME_SHOWING + | FLAG_RESET_FILL_DIALOG_STATE); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; this.mDelayedFillIntentSender = delayedFillIntentSender; @@ -473,7 +483,8 @@ public final class FillRequest implements Parcelable { | FLAG_PASSWORD_INPUT_TYPE | FLAG_VIEW_NOT_FOCUSED | FLAG_SUPPORTS_FILL_DIALOG - | FLAG_IME_SHOWING); + | FLAG_IME_SHOWING + | FLAG_RESET_FILL_DIALOG_STATE); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; this.mDelayedFillIntentSender = delayedFillIntentSender; @@ -495,10 +506,10 @@ public final class FillRequest implements Parcelable { }; @DataClass.Generated( - time = 1647856966565L, + time = 1663290803064L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java", - inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_RESET_FILL_DIALOG_STATE\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java new file mode 100644 index 000000000000..85f5056e4116 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -0,0 +1,181 @@ +/* + * 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 android.view.accessibility; + +import android.accessibilityservice.AccessibilityGestureEvent; +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.MagnificationConfig; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.graphics.Region; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; + +import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback; +import com.android.internal.inputmethod.RemoteAccessibilityInputConnection; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Allows a privileged app - an app with MANAGE_ACCESSIBILITY permission and SystemAPI access - to + * interact with the windows in the display that this proxy represents. Proxying the default display + * or a display that is not tracked will throw an exception. Only the real user has access to global + * clients like SystemUI. + * + * <p> + * To register and unregister a proxy, use + * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)} + * and {@link AccessibilityManager#unregisterDisplayProxy(AccessibilityDisplayProxy)}. If the app + * that has registered the proxy dies, the system will remove the proxy. + * + * TODO(241429275): Complete proxy impl and add additional support (if necessary) like cache methods + * @hide + */ +@SystemApi +public abstract class AccessibilityDisplayProxy { + private static final String LOG_TAG = "AccessibilityDisplayProxy"; + private static final int INVALID_CONNECTION_ID = -1; + + private List<AccessibilityServiceInfo> mInstalledAndEnabledServices; + private Executor mExecutor; + private int mConnectionId = INVALID_CONNECTION_ID; + private int mDisplayId; + IAccessibilityServiceClient mServiceClient; + + /** + * Constructs an AccessibilityDisplayProxy instance. + * @param displayId the id of the display to proxy. + * @param executor the executor used to execute proxy callbacks. + * @param installedAndEnabledServices the list of infos representing the installed and + * enabled a11y services. + */ + public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor, + @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { + mDisplayId = displayId; + mExecutor = executor; + // Typically, the context is the Service context of an accessibility service. + // Context is used for ResolveInfo check, which a proxy won't have, IME input + // (FLAG_INPUT_METHOD_EDITOR), which the proxy doesn't need, and tracing + // A11yInteractionClient methods. + // TODO(254097475): Enable tracing, potentially without exposing Context. + mServiceClient = new IAccessibilityServiceClientImpl(null, mExecutor); + mInstalledAndEnabledServices = installedAndEnabledServices; + } + + /** + * Returns the id of the display being proxy-ed. + */ + public int getDisplayId() { + return mDisplayId; + } + + /** + * An IAccessibilityServiceClient that handles interrupts and accessibility events. + */ + private class IAccessibilityServiceClientImpl extends + AccessibilityService.IAccessibilityServiceClientWrapper { + + IAccessibilityServiceClientImpl(Context context, Executor executor) { + super(context, executor, new AccessibilityService.Callbacks() { + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + // TODO: call AccessiiblityProxy.onAccessibilityEvent + } + + @Override + public void onInterrupt() { + // TODO: call AccessiiblityProxy.onInterrupt + } + @Override + public void onServiceConnected() { + // TODO: send service infos and call AccessiiblityProxy.onProxyConnected + } + @Override + public void init(int connectionId, IBinder windowToken) { + mConnectionId = connectionId; + } + + @Override + public boolean onGesture(AccessibilityGestureEvent gestureInfo) { + return false; + } + + @Override + public boolean onKeyEvent(KeyEvent event) { + return false; + } + + @Override + public void onMagnificationChanged(int displayId, @NonNull Region region, + MagnificationConfig config) { + } + + @Override + public void onMotionEvent(MotionEvent event) { + } + + @Override + public void onTouchStateChanged(int displayId, int state) { + } + + @Override + public void onSoftKeyboardShowModeChanged(int showMode) { + } + + @Override + public void onPerformGestureResult(int sequence, boolean completedSuccessfully) { + } + + @Override + public void onFingerprintCapturingGesturesChanged(boolean active) { + } + + @Override + public void onFingerprintGesture(int gesture) { + } + + @Override + public void onAccessibilityButtonClicked(int displayId) { + } + + @Override + public void onAccessibilityButtonAvailabilityChanged(boolean available) { + } + + @Override + public void onSystemActionsChanged() { + } + + @Override + public void createImeSession(IAccessibilityInputMethodSessionCallback callback) { + } + + @Override + public void startInput(@Nullable RemoteAccessibilityInputConnection inputConnection, + @NonNull EditorInfo editorInfo, boolean restarting) { + } + }); + } + } +} diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 5433fa08ac18..423c560d5c57 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -1921,6 +1921,67 @@ public final class AccessibilityManager { } } + /** + * Registers an {@link AccessibilityDisplayProxy}, so this proxy can access UI content specific + * to its display. + * + * @param proxy the {@link AccessibilityDisplayProxy} to register. + * @return {@code true} if the proxy is successfully registered. + * + * @throws IllegalArgumentException if the proxy's display is not currently tracked by a11y, is + * {@link android.view.Display#DEFAULT_DISPLAY}, is or lower than + * {@link android.view.Display#INVALID_DISPLAY}, or is already being proxy-ed. + * + * @throws SecurityException if the app does not hold the + * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission. + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) + public boolean registerDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + + try { + return service.registerProxyForDisplay(proxy.mServiceClient, proxy.getDisplayId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Unregisters an {@link AccessibilityDisplayProxy}. + * + * @return {@code true} if the proxy is successfully unregistered. + * + * @throws SecurityException if the app does not hold the + * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission. + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) + public boolean unregisterDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + try { + return service.unregisterProxyForDisplay(proxy.getDisplayId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + private IAccessibilityManager getServiceLocked() { if (mService == null) { tryConnectToServiceLocked(null); diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 36fdcce4e1f2..a25194804987 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -109,9 +109,9 @@ interface IAccessibilityManager { oneway void setAccessibilityWindowAttributes(int displayId, int windowId, int userId, in AccessibilityWindowAttributes attributes); - // Requires Manifest.permission.MANAGE_ACCESSIBILITY + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") boolean registerProxyForDisplay(IAccessibilityServiceClient proxy, int displayId); - // Requires Manifest.permission.MANAGE_ACCESSIBILITY + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") boolean unregisterProxyForDisplay(int displayId); } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 70cfc3efc88a..ef683b70172c 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -19,6 +19,7 @@ package android.view.autofill; import static android.service.autofill.FillRequest.FLAG_IME_SHOWING; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; +import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE; import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; import static android.view.ContentInfo.SOURCE_AUTOFILL; @@ -734,7 +735,7 @@ public final class AutofillManager { * Autofill will automatically trigger a fill request after activity * start if there is any field is autofillable. But if there is a field that * triggered autofill, it is unnecessary to trigger again through - * AutofillManager#notifyViewEnteredForActivityStarted. + * AutofillManager#notifyViewEnteredForFillDialog. */ private AtomicBoolean mIsFillRequested; @@ -747,6 +748,10 @@ public final class AutofillManager { private final String[] mFillDialogEnabledHints; + // Tracked all views that have appeared, including views that there are no + // dataset in responses. Used to avoid request pre-fill request again and again. + private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>(); + /** @hide */ public interface AutofillClient { /** @@ -1192,6 +1197,16 @@ public final class AutofillManager { * @hide */ public void notifyViewEnteredForFillDialog(View v) { + synchronized (mLock) { + if (mTrackedViews != null) { + // To support the fill dialog can show for the autofillable Views in + // different pages but in the same Activity. We need to reset the + // mIsFillRequested flag to allow asking for a new FillRequest when + // user switches to other page + mTrackedViews.checkViewState(v.getAutofillId()); + } + } + // Skip if the fill request has been performed for a view. if (mIsFillRequested.get()) { return; @@ -1318,6 +1333,10 @@ public final class AutofillManager { } mForAugmentedAutofillOnly = false; } + + if ((flags & FLAG_SUPPORTS_FILL_DIALOG) != 0) { + flags |= FLAG_RESET_FILL_DIALOG_STATE; + } updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags); } addEnteredIdLocked(id); @@ -2217,6 +2236,7 @@ public final class AutofillManager { mIsFillRequested.set(false); mShowAutofillDialogCalled = false; mFillDialogTriggerIds = null; + mAllTrackedViews.clear(); if (resetEnteredIds) { mEnteredIds = null; } @@ -2776,14 +2796,9 @@ public final class AutofillManager { + ", mFillableIds=" + mFillableIds + ", mEnabled=" + mEnabled + ", mSessionId=" + mSessionId); - } + if (mEnabled && mSessionId == sessionId) { - if (saveOnAllViewsInvisible) { - mTrackedViews = new TrackedViews(trackedIds); - } else { - mTrackedViews = null; - } mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { @@ -2805,6 +2820,27 @@ public final class AutofillManager { mSaveTriggerId = saveTriggerId; setNotifyOnClickLocked(mSaveTriggerId, true); } + + if (!saveOnAllViewsInvisible) { + trackedIds = null; + } + + final ArraySet<AutofillId> allFillableIds = new ArraySet<>(); + if (mFillableIds != null) { + allFillableIds.addAll(mFillableIds); + } + if (trackedIds != null) { + for (AutofillId id : trackedIds) { + id.resetSessionId(); + allFillableIds.add(id); + } + } + + if (!allFillableIds.isEmpty()) { + mTrackedViews = new TrackedViews(trackedIds, Helper.toArray(allFillableIds)); + } else { + mTrackedViews = null; + } } } } @@ -3576,10 +3612,19 @@ public final class AutofillManager { */ private class TrackedViews { /** Visible tracked views */ - @Nullable private ArraySet<AutofillId> mVisibleTrackedIds; + @NonNull private final ArraySet<AutofillId> mVisibleTrackedIds; /** Invisible tracked views */ - @Nullable private ArraySet<AutofillId> mInvisibleTrackedIds; + @NonNull private final ArraySet<AutofillId> mInvisibleTrackedIds; + + /** Visible tracked views for fill dialog */ + @NonNull private final ArraySet<AutofillId> mVisibleDialogTrackedIds; + + /** Invisible tracked views for fill dialog */ + @NonNull private final ArraySet<AutofillId> mInvisibleDialogTrackedIds; + + boolean mHasNewTrackedView; + boolean mIsTrackedSaveView; /** * Check if set is null or value is in set. @@ -3645,43 +3690,65 @@ public final class AutofillManager { * * @param trackedIds The views to be tracked */ - TrackedViews(@Nullable AutofillId[] trackedIds) { - final AutofillClient client = getClient(); - if (!ArrayUtils.isEmpty(trackedIds) && client != null) { - final boolean[] isVisible; - - if (client.autofillClientIsVisibleForAutofill()) { - if (sVerbose) Log.v(TAG, "client is visible, check tracked ids"); - isVisible = client.autofillClientGetViewVisibility(trackedIds); - } else { - // All false - isVisible = new boolean[trackedIds.length]; - } - - final int numIds = trackedIds.length; - for (int i = 0; i < numIds; i++) { - final AutofillId id = trackedIds[i]; - id.resetSessionId(); + TrackedViews(@Nullable AutofillId[] trackedIds, @Nullable AutofillId[] allTrackedIds) { + mVisibleTrackedIds = new ArraySet<>(); + mInvisibleTrackedIds = new ArraySet<>(); + if (!ArrayUtils.isEmpty(trackedIds)) { + mIsTrackedSaveView = true; + initialTrackedViews(trackedIds, mVisibleTrackedIds, mInvisibleTrackedIds); + } - if (isVisible[i]) { - mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id); - } else { - mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id); - } - } + mVisibleDialogTrackedIds = new ArraySet<>(); + mInvisibleDialogTrackedIds = new ArraySet<>(); + if (!ArrayUtils.isEmpty(allTrackedIds)) { + initialTrackedViews(allTrackedIds, mVisibleDialogTrackedIds, + mInvisibleDialogTrackedIds); + mAllTrackedViews.addAll(Arrays.asList(allTrackedIds)); } if (sVerbose) { Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): " + " mVisibleTrackedIds=" + mVisibleTrackedIds - + " mInvisibleTrackedIds=" + mInvisibleTrackedIds); + + " mInvisibleTrackedIds=" + mInvisibleTrackedIds + + " allTrackedIds=" + Arrays.toString(allTrackedIds) + + " mVisibleDialogTrackedIds=" + mVisibleDialogTrackedIds + + " mInvisibleDialogTrackedIds=" + mInvisibleDialogTrackedIds); } - if (mVisibleTrackedIds == null) { + if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); } } + private void initialTrackedViews(AutofillId[] trackedIds, + @NonNull ArraySet<AutofillId> visibleSet, + @NonNull ArraySet<AutofillId> invisibleSet) { + final boolean[] isVisible; + final AutofillClient client = getClient(); + if (ArrayUtils.isEmpty(trackedIds) || client == null) { + return; + } + if (client.autofillClientIsVisibleForAutofill()) { + if (sVerbose) Log.v(TAG, "client is visible, check tracked ids"); + isVisible = client.autofillClientGetViewVisibility(trackedIds); + } else { + // All false + isVisible = new boolean[trackedIds.length]; + } + + final int numIds = trackedIds.length; + for (int i = 0; i < numIds; i++) { + final AutofillId id = trackedIds[i]; + id.resetSessionId(); + + if (isVisible[i]) { + addToSet(visibleSet, id); + } else { + addToSet(invisibleSet, id); + } + } + } + /** * Called when a {@link View view's} visibility changes. * @@ -3698,22 +3765,37 @@ public final class AutofillManager { if (isClientVisibleForAutofillLocked()) { if (isVisible) { if (isInSet(mInvisibleTrackedIds, id)) { - mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id); - mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id); + removeFromSet(mInvisibleTrackedIds, id); + addToSet(mVisibleTrackedIds, id); + } + if (isInSet(mInvisibleDialogTrackedIds, id)) { + removeFromSet(mInvisibleDialogTrackedIds, id); + addToSet(mVisibleDialogTrackedIds, id); } } else { if (isInSet(mVisibleTrackedIds, id)) { - mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id); - mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id); + removeFromSet(mVisibleTrackedIds, id); + addToSet(mInvisibleTrackedIds, id); + } + if (isInSet(mVisibleDialogTrackedIds, id)) { + removeFromSet(mVisibleDialogTrackedIds, id); + addToSet(mInvisibleDialogTrackedIds, id); } } } - if (mVisibleTrackedIds == null) { + if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { if (sVerbose) { Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleTrackedIds); } finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); + + } + if (mVisibleDialogTrackedIds.isEmpty()) { + if (sVerbose) { + Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleDialogTrackedIds); + } + processNoVisibleTrackedAllViews(); } } @@ -3727,66 +3809,66 @@ public final class AutofillManager { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. AutofillClient client = getClient(); - ArraySet<AutofillId> updatedVisibleTrackedIds = null; - ArraySet<AutofillId> updatedInvisibleTrackedIds = null; if (client != null) { if (sVerbose) { Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds + " vis=" + mVisibleTrackedIds); } - if (mInvisibleTrackedIds != null) { - final ArrayList<AutofillId> orderedInvisibleIds = - new ArrayList<>(mInvisibleTrackedIds); - final boolean[] isVisible = client.autofillClientGetViewVisibility( - Helper.toArray(orderedInvisibleIds)); - - final int numInvisibleTrackedIds = orderedInvisibleIds.size(); - for (int i = 0; i < numInvisibleTrackedIds; i++) { - final AutofillId id = orderedInvisibleIds.get(i); - if (isVisible[i]) { - updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id); - if (sDebug) { - Log.d(TAG, "onVisibleForAutofill() " + id + " became visible"); - } - } else { - updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id); - } - } - } + onVisibleForAutofillChangedInternalLocked(mVisibleTrackedIds, mInvisibleTrackedIds); + onVisibleForAutofillChangedInternalLocked( + mVisibleDialogTrackedIds, mInvisibleDialogTrackedIds); + } - if (mVisibleTrackedIds != null) { - final ArrayList<AutofillId> orderedVisibleIds = - new ArrayList<>(mVisibleTrackedIds); - final boolean[] isVisible = client.autofillClientGetViewVisibility( - Helper.toArray(orderedVisibleIds)); + if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { + if (sVerbose) { + Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); + } + finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); + } + if (mVisibleDialogTrackedIds.isEmpty()) { + if (sVerbose) { + Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); + } + processNoVisibleTrackedAllViews(); + } + } - final int numVisibleTrackedIds = orderedVisibleIds.size(); - for (int i = 0; i < numVisibleTrackedIds; i++) { - final AutofillId id = orderedVisibleIds.get(i); + void onVisibleForAutofillChangedInternalLocked(@NonNull ArraySet<AutofillId> visibleSet, + @NonNull ArraySet<AutofillId> invisibleSet) { + // The visibility of the views might have changed while the client was not be visible, + // hence update the visibility state for all views. + if (sVerbose) { + Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + invisibleSet + + " vis=" + visibleSet); + } - if (isVisible[i]) { - updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id); - } else { - updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id); + ArraySet<AutofillId> allTrackedIds = new ArraySet<>(); + allTrackedIds.addAll(visibleSet); + allTrackedIds.addAll(invisibleSet); + if (!allTrackedIds.isEmpty()) { + visibleSet.clear(); + invisibleSet.clear(); + initialTrackedViews(Helper.toArray(allTrackedIds), visibleSet, invisibleSet); + } + } - if (sDebug) { - Log.d(TAG, "onVisibleForAutofill() " + id + " became invisible"); - } - } - } - } + private void processNoVisibleTrackedAllViews() { + mShowAutofillDialogCalled = false; + } - mInvisibleTrackedIds = updatedInvisibleTrackedIds; - mVisibleTrackedIds = updatedVisibleTrackedIds; + void checkViewState(AutofillId id) { + if (mAllTrackedViews.contains(id)) { + return; } - - if (mVisibleTrackedIds == null) { - if (sVerbose) { - Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); - } - finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); + // Add the id as tracked to avoid triggering fill request again and again. + mAllTrackedViews.add(id); + if (mHasNewTrackedView) { + return; } + // First one new tracks view + mIsFillRequested.set(false); + mHasNewTrackedView = true; } } diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 1664637eac56..d067d4bc366b 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -378,7 +378,7 @@ public final class ContentCaptureManager { private final Object mLock = new Object(); @NonNull - private final Context mContext; + private final StrippedContext mContext; @NonNull private final IContentCaptureManager mService; @@ -414,9 +414,37 @@ public final class ContentCaptureManager { } /** @hide */ + static class StrippedContext { + final String mPackageName; + final String mContext; + final @UserIdInt int mUserId; + + private StrippedContext(Context context) { + mPackageName = context.getPackageName(); + mContext = context.toString(); + mUserId = context.getUserId(); + } + + @Override + public String toString() { + return mContext; + } + + public String getPackageName() { + return mPackageName; + } + + @UserIdInt + public int getUserId() { + return mUserId; + } + } + + /** @hide */ public ContentCaptureManager(@NonNull Context context, @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) { - mContext = Objects.requireNonNull(context, "context cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + mContext = new StrippedContext(context); mService = Objects.requireNonNull(service, "service cannot be null"); mOptions = Objects.requireNonNull(options, "options cannot be null"); diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index c32ca9e2e215..a98955862d7b 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -36,7 +36,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.ComponentName; -import android.content.Context; import android.content.pm.ParceledListSlice; import android.graphics.Insets; import android.graphics.Rect; @@ -103,7 +102,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { private final AtomicBoolean mDisabled = new AtomicBoolean(false); @NonNull - private final Context mContext; + private final ContentCaptureManager.StrippedContext mContext; @NonNull private final ContentCaptureManager mManager; @@ -197,7 +196,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } - protected MainContentCaptureSession(@NonNull Context context, + protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext context, @NonNull ContentCaptureManager manager, @NonNull Handler handler, @NonNull IContentCaptureManager systemServerInterface) { mContext = context; diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index 7f859d6b4bf4..1afa98769a5a 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -489,6 +489,7 @@ final class IInputMethodManagerGlobalInvoker { } @AnyThread + @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) static void addVirtualStylusIdForTestSession(IInputMethodClient client) { final IInputMethodManager service = getService(); if (service == null) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index eb72405b3051..9106ce27c27c 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2596,6 +2596,7 @@ public final class InputMethodManager { * @hide */ @TestApi + @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession() { synchronized (mH) { IInputMethodManagerGlobalInvoker.addVirtualStylusIdForTestSession(mClient); diff --git a/core/java/android/view/inputmethod/InsertGesture.java b/core/java/android/view/inputmethod/InsertGesture.java index 9f0328909190..0449a16d785d 100644 --- a/core/java/android/view/inputmethod/InsertGesture.java +++ b/core/java/android/view/inputmethod/InsertGesture.java @@ -21,7 +21,6 @@ import android.annotation.SuppressLint; import android.graphics.PointF; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; import android.widget.TextView; import androidx.annotation.Nullable; @@ -52,7 +51,8 @@ public final class InsertGesture extends HandwritingGesture implements Parcelabl mPoint = source.readTypedObject(PointF.CREATOR); } - /** Returns the text that will be inserted at {@link #getInsertionPoint()} **/ + /** Returns the text that will be inserted at {@link #getInsertionPoint()}. When text is + * empty, cursor should be moved the insertion point. **/ @NonNull public String getTextToInsert() { return mTextToInsert; @@ -75,7 +75,11 @@ public final class InsertGesture extends HandwritingGesture implements Parcelabl private PointF mPoint; private String mFallbackText; - /** set the text that will be inserted at {@link #setInsertionPoint(PointF)} **/ + /** + * Set the text that will be inserted at {@link #setInsertionPoint(PointF)}. When set with + * an empty string, cursor will be moved to {@link #getInsertionPoint()} and no text + * would be inserted. + */ @NonNull @SuppressLint("MissingGetterMatchingBuilder") public Builder setTextToInsert(@NonNull String text) { @@ -114,8 +118,8 @@ public final class InsertGesture extends HandwritingGesture implements Parcelabl if (mPoint == null) { throw new IllegalArgumentException("Insertion point must be set."); } - if (TextUtils.isEmpty(mText)) { - throw new IllegalArgumentException("Text to insert must be non-empty."); + if (mText == null) { + throw new IllegalArgumentException("Text to insert must be set."); } return new InsertGesture(mText, mPoint, mFallbackText); } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 423642acf2f7..f7bb16e8ba54 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -147,9 +147,9 @@ interface IInputMethodManager { boolean isStylusHandwritingAvailableAsUser(int userId); /** add virtual stylus id for test Stylus handwriting session **/ - @EnforcePermission("INJECT_EVENTS") + @EnforcePermission("TEST_INPUT_METHOD") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.INJECT_EVENTS)") + + "android.Manifest.permission.TEST_INPUT_METHOD)") void addVirtualStylusIdForTestSession(in IInputMethodClient client); /** Set a stylus idle-timeout after which handwriting {@code InkWindow} will be removed. */ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d8ecb5c65384..16e0a5967e78 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1150,6 +1150,18 @@ android:description="@string/permdesc_readMediaImages" android:protectionLevel="dangerous" /> + <!-- Allows an application to read image or video files from external storage that a user has + selected via the permission prompt photo picker. Apps can check this permission to verify that + a user has decided to use the photo picker, instead of granting access to + {@link #READ_MEDIA_IMAGES or #READ_MEDIA_VIDEO}. It does not prevent apps from accessing the + standard photo picker manually. + <p>Protection level: dangerous --> + <permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_readVisualUserSelect" + android:description="@string/permdesc_readVisualUserSelect" + android:protectionLevel="dangerous" /> + <!-- Allows an application to write to external storage. <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or higher, this permission has no effect. diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 509de3364f0e..1f459c6f623d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1933,6 +1933,11 @@ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] --> <string name="permdesc_readMediaImages">Allows the app to read image files from your shared storage.</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] --> + <string name="permlab_readVisualUserSelect">read user selected image and video files from shared storage</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] --> + <string name="permdesc_readVisualUserSelect">Allows the app to read image and video files that you select from your shared storage.</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] --> <string name="permlab_sdcardWrite">modify or delete the contents of your shared storage</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] --> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java index 57b9cb1a9097..5bd018bea1d1 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java @@ -23,11 +23,13 @@ import static org.junit.Assert.assertThrows; import android.annotation.Nullable; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; +import android.os.Parcel; import org.junit.Test; public final class ProgramSelectorTest { + private static final int CREATOR_ARRAY_SIZE = 2; private static final int FM_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_FM; private static final int DAB_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_DAB; private static final long FM_FREQUENCY = 88500; @@ -97,6 +99,33 @@ public final class ProgramSelectorTest { } @Test + public void describeContents_forIdentifier() { + assertWithMessage("FM identifier contents") + .that(FM_IDENTIFIER.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forIdentifierCreator() { + ProgramSelector.Identifier[] identifiers = + ProgramSelector.Identifier.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Identifiers").that(identifiers).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test + public void writeToParcel_forIdentifier() { + Parcel parcel = Parcel.obtain(); + + FM_IDENTIFIER.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + ProgramSelector.Identifier identifierFromParcel = + ProgramSelector.Identifier.CREATOR.createFromParcel(parcel); + assertWithMessage("Identifier created from parcel") + .that(identifierFromParcel).isEqualTo(FM_IDENTIFIER); + } + + @Test public void getProgramType() { ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null); @@ -394,6 +423,34 @@ public final class ProgramSelectorTest { .that(selector1.strictEquals(selector2)).isTrue(); } + @Test + public void describeContents_forProgramSelector() { + assertWithMessage("FM selector contents") + .that(getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null) + .describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forProgramSelectorCreator() { + ProgramSelector[] programSelectors = ProgramSelector.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Program selectors").that(programSelectors).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test + public void writeToParcel_forProgramSelector() { + ProgramSelector selectorExpected = + getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null); + Parcel parcel = Parcel.obtain(); + + selectorExpected.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + ProgramSelector selectorFromParcel = ProgramSelector.CREATOR.createFromParcel(parcel); + assertWithMessage("Program selector created from parcel") + .that(selectorFromParcel).isEqualTo(selectorExpected); + } + private ProgramSelector getFmSelector(@Nullable ProgramSelector.Identifier[] secondaryIds, @Nullable long[] vendorIds) { return new ProgramSelector(FM_PROGRAM_TYPE, FM_IDENTIFIER, secondaryIds, vendorIds); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java index 42143b92e9d8..6e1bb4b4c5a3 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import android.hardware.radio.Announcement; import android.hardware.radio.ProgramSelector; +import android.os.Parcel; import android.util.ArrayMap; import org.junit.Test; @@ -83,4 +84,35 @@ public final class RadioAnnouncementTest { vendorInfo.put("vendorKeyMock", "vendorValueMock"); return vendorInfo; } + + @Test + public void describeContents_forAnnouncement() { + assertWithMessage("Radio announcement contents") + .that(TEST_ANNOUNCEMENT.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forAnnouncementCreator() { + int sizeExpected = 2; + + Announcement[] announcements = Announcement.CREATOR.newArray(sizeExpected); + + assertWithMessage("Announcements").that(announcements).hasLength(sizeExpected); + } + + @Test + public void writeToParcel_forAnnouncement() { + Parcel parcel = Parcel.obtain(); + + TEST_ANNOUNCEMENT.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + Announcement announcementFromParcel = Announcement.CREATOR.createFromParcel(parcel); + assertWithMessage("Selector of announcement created from parcel") + .that(announcementFromParcel.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR); + assertWithMessage("Type of announcement created from parcel") + .that(announcementFromParcel.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE); + assertWithMessage("Vendor info of announcement created from parcel") + .that(announcementFromParcel.getVendorInfo()).isEqualTo(VENDOR_INFO); + } } diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java index be4d0d434d79..f838a5df2eae 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java @@ -33,6 +33,7 @@ import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.RadioTuner; +import android.os.Parcel; import android.os.RemoteException; import android.util.ArrayMap; @@ -80,6 +81,8 @@ public final class RadioManagerTest { private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI}; + private static final int CREATOR_ARRAY_SIZE = 3; + private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = createFmBandDescriptor(); private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR = @@ -173,6 +176,22 @@ public final class RadioManagerTest { } @Test + public void describeContents_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor(); + + assertWithMessage("Band Descriptor contents") + .that(bandDescriptor.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forBandDescriptorCreator() { + RadioManager.BandDescriptor[] bandDescriptors = + RadioManager.BandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Band Descriptors").that(bandDescriptors).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void isAmBand_forAmBandDescriptor_returnsTrue() { RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); @@ -219,18 +238,73 @@ public final class RadioManagerTest { } @Test + public void describeContents_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor contents") + .that(FM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0); + } + + @Test + public void writeToParcel_forFmBandDescriptor() { + Parcel parcel = Parcel.obtain(); + + FM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioManager.FmBandDescriptor fmBandDescriptorFromParcel = + RadioManager.FmBandDescriptor.CREATOR.createFromParcel(parcel); + assertWithMessage("FM Band Descriptor created from parcel") + .that(fmBandDescriptorFromParcel).isEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void newArray_forFmBandDescriptorCreator() { + RadioManager.FmBandDescriptor[] fmBandDescriptors = + RadioManager.FmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("FM Band Descriptors") + .that(fmBandDescriptors).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void isStereoSupported_forAmBandDescriptor() { assertWithMessage("AM Band Descriptor stereo") .that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED); } @Test + public void describeContents_forAmBandDescriptor() { + assertWithMessage("AM Band Descriptor contents") + .that(AM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0); + } + + @Test + public void writeToParcel_forAmBandDescriptor() { + Parcel parcel = Parcel.obtain(); + + AM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioManager.AmBandDescriptor amBandDescriptorFromParcel = + RadioManager.AmBandDescriptor.CREATOR.createFromParcel(parcel); + assertWithMessage("FM Band Descriptor created from parcel") + .that(amBandDescriptorFromParcel).isEqualTo(AM_BAND_DESCRIPTOR); + } + + @Test + public void newArray_forAmBandDescriptorCreator() { + RadioManager.AmBandDescriptor[] amBandDescriptors = + RadioManager.AmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("AM Band Descriptors") + .that(amBandDescriptors).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void equals_withSameFmBandDescriptors_returnsTrue() { - RadioManager.FmBandDescriptor fmBandDescriptor1 = createFmBandDescriptor(); - RadioManager.FmBandDescriptor fmBandDescriptor2 = createFmBandDescriptor(); + RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor(); assertWithMessage("The same FM Band Descriptor") - .that(fmBandDescriptor1).isEqualTo(fmBandDescriptor2); + .that(FM_BAND_DESCRIPTOR).isEqualTo(fmBandDescriptorCompared); } @Test @@ -258,6 +332,44 @@ public final class RadioManagerTest { } @Test + public void hashCode_withSameFmBandDescriptors_equals() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor(); + + assertWithMessage("Hash code of the same FM Band Descriptor") + .that(fmBandDescriptorCompared.hashCode()).isEqualTo(FM_BAND_DESCRIPTOR.hashCode()); + } + + @Test + public void hashCode_withSameAmBandDescriptors_equals() { + RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor(); + + assertWithMessage("Hash code of the same AM Band Descriptor") + .that(amBandDescriptorCompared.hashCode()).isEqualTo(AM_BAND_DESCRIPTOR.hashCode()); + } + + @Test + public void hashCode_withFmBandDescriptorsOfDifferentAfSupports_notEquals() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, !AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("Hash code of FM Band Descriptor of different spacing") + .that(fmBandDescriptorCompared.hashCode()) + .isNotEqualTo(FM_BAND_DESCRIPTOR.hashCode()); + } + + @Test + public void hashCode_withAmBandDescriptorsOfDifferentSpacings_notEquals() { + RadioManager.AmBandDescriptor amBandDescriptorCompared = + new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING * 2, STEREO_SUPPORTED); + + assertWithMessage("Hash code of AM Band Descriptor of different spacing") + .that(amBandDescriptorCompared.hashCode()) + .isNotEqualTo(AM_BAND_DESCRIPTOR.hashCode()); + } + + @Test public void getType_forBandConfig() { RadioManager.BandConfig fmBandConfig = createFmBandConfig(); @@ -298,8 +410,24 @@ public final class RadioManagerTest { } @Test + public void describeContents_forBandConfig() { + RadioManager.BandConfig bandConfig = createFmBandConfig(); + + assertWithMessage("FM Band Config contents") + .that(bandConfig.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forBandConfigCreator() { + RadioManager.BandConfig[] bandConfigs = + RadioManager.BandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Band Configs").that(bandConfigs).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void getStereo_forFmBandConfig() { - assertWithMessage("FM Band Config stereo ") + assertWithMessage("FM Band Config stereo") .that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED); } @@ -328,12 +456,66 @@ public final class RadioManagerTest { } @Test + public void describeContents_forFmBandConfig() { + assertWithMessage("FM Band Config contents") + .that(FM_BAND_CONFIG.describeContents()).isEqualTo(0); + } + + @Test + public void writeToParcel_forFmBandConfig() { + Parcel parcel = Parcel.obtain(); + + FM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioManager.FmBandConfig fmBandConfigFromParcel = + RadioManager.FmBandConfig.CREATOR.createFromParcel(parcel); + assertWithMessage("FM Band Config created from parcel") + .that(fmBandConfigFromParcel).isEqualTo(FM_BAND_CONFIG); + } + + @Test + public void newArray_forFmBandConfigCreator() { + RadioManager.FmBandConfig[] fmBandConfigs = + RadioManager.FmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("FM Band Configs").that(fmBandConfigs).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void getStereo_forAmBandConfig() { assertWithMessage("AM Band Config stereo") .that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED); } @Test + public void describeContents_forAmBandConfig() { + assertWithMessage("AM Band Config contents") + .that(AM_BAND_CONFIG.describeContents()).isEqualTo(0); + } + + @Test + public void writeToParcel_forAmBandConfig() { + Parcel parcel = Parcel.obtain(); + + AM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioManager.AmBandConfig amBandConfigFromParcel = + RadioManager.AmBandConfig.CREATOR.createFromParcel(parcel); + assertWithMessage("AM Band Config created from parcel") + .that(amBandConfigFromParcel).isEqualTo(AM_BAND_CONFIG); + } + + @Test + public void newArray_forAmBandConfigCreator() { + RadioManager.AmBandConfig[] amBandConfigs = + RadioManager.AmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("AM Band Configs").that(amBandConfigs).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void equals_withSameFmBandConfigs_returnsTrue() { RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig(); @@ -387,6 +569,43 @@ public final class RadioManagerTest { } @Test + public void hashCode_withSameFmBandConfigs_equals() { + RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig(); + + assertWithMessage("Hash code of the same FM Band Config") + .that(FM_BAND_CONFIG.hashCode()).isEqualTo(fmBandConfigCompared.hashCode()); + } + + @Test + public void hashCode_withSameAmBandConfigs_equals() { + RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig(); + + assertWithMessage("Hash code of the same AM Band Config") + .that(amBandConfigCompared.hashCode()).isEqualTo(AM_BAND_CONFIG.hashCode()); + } + + @Test + public void hashCode_withFmBandConfigsOfDifferentTypes_notEquals() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM_HD, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, + AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("Hash code of FM Band Config with different type") + .that(fmBandConfigCompared.hashCode()).isNotEqualTo(FM_BAND_CONFIG.hashCode()); + } + + @Test + public void hashCode_withAmBandConfigsOfDifferentStereoSupports_notEquals() { + RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig( + new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING, !STEREO_SUPPORTED)); + + assertWithMessage("Hash code of AM Band Config with different stereo support") + .that(amBandConfigCompared.hashCode()).isNotEqualTo(AM_BAND_CONFIG.hashCode()); + } + + @Test public void getId_forModuleProperties() { assertWithMessage("Properties id") .that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID); @@ -509,6 +728,12 @@ public final class RadioManagerTest { } @Test + public void describeContents_forModuleProperties() { + assertWithMessage("Module properties contents") + .that(AMFM_PROPERTIES.describeContents()).isEqualTo(0); + } + + @Test public void equals_withSameProperties_returnsTrue() { RadioManager.ModuleProperties propertiesCompared = createAmFmProperties(); @@ -530,6 +755,23 @@ public final class RadioManagerTest { } @Test + public void hashCode_withSameModuleProperties_equals() { + RadioManager.ModuleProperties propertiesCompared = createAmFmProperties(); + + assertWithMessage("Hash code of the same module properties") + .that(propertiesCompared.hashCode()).isEqualTo(AMFM_PROPERTIES.hashCode()); + } + + @Test + public void newArray_forModulePropertiesCreator() { + RadioManager.ModuleProperties[] modulePropertiesArray = + RadioManager.ModuleProperties.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Module properties array") + .that(modulePropertiesArray).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test public void getSelector_forProgramInfo() { assertWithMessage("Selector of DAB program info") .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR); @@ -549,7 +791,7 @@ public final class RadioManagerTest { @Test public void getRelatedContent_forProgramInfo() { - assertWithMessage("Related contents of DAB program info") + assertWithMessage("DAB program info contents") .that(DAB_PROGRAM_INFO.getRelatedContent()) .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED); } @@ -627,6 +869,33 @@ public final class RadioManagerTest { } @Test + public void describeContents_forProgramInfo() { + assertWithMessage("Program info contents") + .that(DAB_PROGRAM_INFO.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forProgramInfoCreator() { + RadioManager.ProgramInfo[] programInfoArray = + RadioManager.ProgramInfo.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Program infos").that(programInfoArray).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test + public void writeToParcel_forProgramInfo() { + Parcel parcel = Parcel.obtain(); + + DAB_PROGRAM_INFO.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioManager.ProgramInfo programInfoFromParcel = + RadioManager.ProgramInfo.CREATOR.createFromParcel(parcel); + assertWithMessage("Program info created from parcel") + .that(programInfoFromParcel).isEqualTo(DAB_PROGRAM_INFO); + } + + @Test public void equals_withSameProgramInfo_returnsTrue() { RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java index fe15597a9514..5771135e32b8 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java @@ -20,18 +20,63 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; +import android.graphics.Bitmap; import android.hardware.radio.RadioMetadata; +import android.os.Parcel; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Set; +@RunWith(MockitoJUnitRunner.class) public final class RadioMetadataTest { + private static final int CREATOR_ARRAY_SIZE = 3; private static final int INT_KEY_VALUE = 1; + private static final long TEST_UTC_SECOND_SINCE_EPOCH = 200; + private static final int TEST_TIME_ZONE_OFFSET_MINUTES = 1; private final RadioMetadata.Builder mBuilder = new RadioMetadata.Builder(); + @Mock + private Bitmap mBitmapValue; + + @Test + public void describeContents_forClock() { + RadioMetadata.Clock clock = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH, + TEST_TIME_ZONE_OFFSET_MINUTES); + + assertWithMessage("Describe contents for metadata clock") + .that(clock.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forClockCreator() { + RadioMetadata.Clock[] clocks = RadioMetadata.Clock.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Clock array size").that(clocks.length).isEqualTo(CREATOR_ARRAY_SIZE); + } + + @Test + public void writeToParcel_forClock() { + RadioMetadata.Clock clockExpected = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH, + TEST_TIME_ZONE_OFFSET_MINUTES); + Parcel parcel = Parcel.obtain(); + + clockExpected.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioMetadata.Clock clockFromParcel = RadioMetadata.Clock.CREATOR.createFromParcel(parcel); + assertWithMessage("UTC second since epoch of metadata clock created from parcel") + .that(clockFromParcel.getUtcEpochSeconds()).isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH); + assertWithMessage("Time zone offset minutes of metadata clock created from parcel") + .that(clockFromParcel.getTimezoneOffsetMinutes()) + .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES); + } + @Test public void putString_withIllegalKey() { String invalidStringKey = RadioMetadata.METADATA_KEY_RDS_PI; @@ -129,22 +174,56 @@ public final class RadioMetadataTest { } @Test + public void getBitmap_withKeyInMetadata() { + String key = RadioMetadata.METADATA_KEY_ICON; + RadioMetadata metadata = mBuilder.putBitmap(key, mBitmapValue).build(); + + assertWithMessage("Bitmap value for key %s in metadata", key) + .that(metadata.getBitmap(key)).isEqualTo(mBitmapValue); + } + + @Test + public void getBitmap_withKeyNotInMetadata() { + String key = RadioMetadata.METADATA_KEY_ICON; + RadioMetadata metadata = mBuilder.build(); + + assertWithMessage("Bitmap value for key %s not in metadata", key) + .that(metadata.getBitmap(key)).isNull(); + } + + @Test + public void getBitmapId_withKeyInMetadata() { + String key = RadioMetadata.METADATA_KEY_ART; + RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build(); + + assertWithMessage("Bitmap id value for key %s in metadata", key) + .that(metadata.getBitmapId(key)).isEqualTo(INT_KEY_VALUE); + } + + @Test + public void getBitmapId_withKeyNotInMetadata() { + String key = RadioMetadata.METADATA_KEY_ART; + RadioMetadata metadata = mBuilder.build(); + + assertWithMessage("Bitmap id value for key %s not in metadata", key) + .that(metadata.getBitmapId(key)).isEqualTo(0); + } + + @Test public void getClock_withKeyInMetadata() { String key = RadioMetadata.METADATA_KEY_CLOCK; - long utcSecondsSinceEpochExpected = 200; - int timezoneOffsetMinutesExpected = 1; RadioMetadata metadata = mBuilder - .putClock(key, utcSecondsSinceEpochExpected, timezoneOffsetMinutesExpected) + .putClock(key, TEST_UTC_SECOND_SINCE_EPOCH, TEST_TIME_ZONE_OFFSET_MINUTES) .build(); RadioMetadata.Clock clockExpected = metadata.getClock(key); assertWithMessage("Number of seconds since epoch of value for key %s in metadata", key) .that(clockExpected.getUtcEpochSeconds()) - .isEqualTo(utcSecondsSinceEpochExpected); + .isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH); assertWithMessage("Offset of timezone in minutes of value for key %s in metadata", key) .that(clockExpected.getTimezoneOffsetMinutes()) - .isEqualTo(timezoneOffsetMinutesExpected); + .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES); } @Test @@ -180,12 +259,13 @@ public final class RadioMetadataTest { RadioMetadata metadata = mBuilder .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE) .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest") + .putBitmap(RadioMetadata.METADATA_KEY_ICON, mBitmapValue) .build(); Set<String> metadataSet = metadata.keySet(); assertWithMessage("Metadata set of non-empty metadata") - .that(metadataSet).containsExactly( + .that(metadataSet).containsExactly(RadioMetadata.METADATA_KEY_ICON, RadioMetadata.METADATA_KEY_RDS_PI, RadioMetadata.METADATA_KEY_ARTIST); } @@ -208,4 +288,46 @@ public final class RadioMetadataTest { .that(key).isEqualTo(RadioMetadata.METADATA_KEY_RDS_PI); } + @Test + public void equals_forMetadataWithSameContents_returnsTrue() { + RadioMetadata metadata = mBuilder + .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE) + .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest") + .build(); + RadioMetadata.Builder copyBuilder = new RadioMetadata.Builder(metadata); + RadioMetadata metadataCopied = copyBuilder.build(); + + assertWithMessage("Metadata with the same contents") + .that(metadataCopied).isEqualTo(metadata); + } + + @Test + public void describeContents_forMetadata() { + RadioMetadata metadata = mBuilder.build(); + + assertWithMessage("Metadata contents").that(metadata.describeContents()).isEqualTo(0); + } + + @Test + public void newArray_forRadioMetadataCreator() { + RadioMetadata[] metadataArray = RadioMetadata.CREATOR.newArray(CREATOR_ARRAY_SIZE); + + assertWithMessage("Radio metadata array").that(metadataArray).hasLength(CREATOR_ARRAY_SIZE); + } + + @Test + public void writeToParcel_forRadioMetadata() { + RadioMetadata metadataExpected = mBuilder + .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE) + .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest") + .build(); + Parcel parcel = Parcel.obtain(); + + metadataExpected.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + + RadioMetadata metadataFromParcel = RadioMetadata.CREATOR.createFromParcel(parcel); + assertWithMessage("Radio metadata created from parcel") + .that(metadataFromParcel).isEqualTo(metadataExpected); + } } diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java index bb1a3b182f91..ee1e10f9009e 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; import android.app.Instrumentation; import android.app.PendingIntent; import android.app.RemoteAction; @@ -34,6 +35,7 @@ import android.content.Intent; import android.graphics.drawable.Icon; import android.os.UserHandle; +import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -51,6 +53,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; /** * Tests for the AccessibilityManager by mocking the backing service. @@ -70,6 +73,7 @@ public class AccessibilityManagerTest { LABEL, DESCRIPTION, TEST_PENDING_INTENT); + private static final int DISPLAY_ID = 22; @Mock private IAccessibilityManager mMockService; private MessageCapturingHandler mHandler; @@ -224,4 +228,45 @@ public class AccessibilityManagerTest { assertEquals(mFocusColorDefaultValue, manager.getAccessibilityFocusColor()); } + + @Test + public void testRegisterAccessibilityProxy() throws Exception { + // Accessibility does not need to be enabled for a proxy to be registered. + AccessibilityManager manager = + new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService, + UserHandle.USER_CURRENT, true); + + + ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>(); + infos.add(new AccessibilityServiceInfo()); + AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos); + manager.registerDisplayProxy(proxy); + // Cannot access proxy.mServiceClient directly due to visibility. + verify(mMockService).registerProxyForDisplay(any(IAccessibilityServiceClient.class), + any(Integer.class)); + } + + @Test + public void testUnregisterAccessibilityProxy() throws Exception { + // Accessibility does not need to be enabled for a proxy to be registered. + final AccessibilityManager manager = + new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService, + UserHandle.USER_CURRENT, true); + + final ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>(); + infos.add(new AccessibilityServiceInfo()); + + final AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos); + manager.registerDisplayProxy(proxy); + manager.unregisterDisplayProxy(proxy); + verify(mMockService).unregisterProxyForDisplay(proxy.getDisplayId()); + } + + private class MyAccessibilityProxy extends AccessibilityDisplayProxy { + // TODO(241429275): Will override A11yProxy methods in the future. + MyAccessibilityProxy(int displayId, + @NonNull List<AccessibilityServiceInfo> serviceInfos) { + super(displayId, Executors.newSingleThreadExecutor(), serviceInfos); + } + } } diff --git a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml new file mode 100644 index 000000000000..8779cc09715b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml new file mode 100644 index 000000000000..ea0fbb0e5d33 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M18.167,21.875H29.833V10.208H18.167ZM7.875,35.833Q6.375,35.833 5.271,34.729Q4.167,33.625 4.167,32.125V7.875Q4.167,6.375 5.271,5.271Q6.375,4.167 7.875,4.167H32.125Q33.625,4.167 34.729,5.271Q35.833,6.375 35.833,7.875V32.125Q35.833,33.625 34.729,34.729Q33.625,35.833 32.125,35.833ZM7.875,32.125H32.125Q32.125,32.125 32.125,32.125Q32.125,32.125 32.125,32.125V7.875Q32.125,7.875 32.125,7.875Q32.125,7.875 32.125,7.875H7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125ZM7.875,7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125V7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml new file mode 100644 index 000000000000..c55cbe2d054c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M34.042,14.625V9.333Q34.042,9.333 34.042,9.333Q34.042,9.333 34.042,9.333H28.708V5.708H33.917Q35.458,5.708 36.562,6.833Q37.667,7.958 37.667,9.458V14.625ZM2.375,14.625V9.458Q2.375,7.958 3.479,6.833Q4.583,5.708 6.125,5.708H11.292V9.333H6Q6,9.333 6,9.333Q6,9.333 6,9.333V14.625ZM28.708,34.25V30.667H34.042Q34.042,30.667 34.042,30.667Q34.042,30.667 34.042,30.667V25.333H37.667V30.542Q37.667,32 36.562,33.125Q35.458,34.25 33.917,34.25ZM6.125,34.25Q4.583,34.25 3.479,33.125Q2.375,32 2.375,30.542V25.333H6V30.667Q6,30.667 6,30.667Q6,30.667 6,30.667H11.292V34.25ZM9.333,27.292V12.667H30.708V27.292ZM12.917,23.708H27.125V16.25H12.917ZM12.917,23.708V16.25V23.708Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml new file mode 100644 index 000000000000..447df43dfddd --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M8.083,22.833Q6.917,22.833 6.104,22Q5.292,21.167 5.292,20Q5.292,18.833 6.125,18Q6.958,17.167 8.125,17.167Q9.292,17.167 10.125,18Q10.958,18.833 10.958,20Q10.958,21.167 10.125,22Q9.292,22.833 8.083,22.833ZM20,22.833Q18.833,22.833 18,22Q17.167,21.167 17.167,20Q17.167,18.833 18,18Q18.833,17.167 20,17.167Q21.167,17.167 22,18Q22.833,18.833 22.833,20Q22.833,21.167 22,22Q21.167,22.833 20,22.833ZM31.875,22.833Q30.708,22.833 29.875,22Q29.042,21.167 29.042,20Q29.042,18.833 29.875,18Q30.708,17.167 31.917,17.167Q33.083,17.167 33.896,18Q34.708,18.833 34.708,20Q34.708,21.167 33.875,22Q33.042,22.833 31.875,22.833Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml new file mode 100644 index 000000000000..c334a543a86a --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:translateX="6.0" + android:translateY="8.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M18 14L13 14L13 2L18 2L18 14ZM20 14L20 2C20 0.9 19.1 -3.93402e-08 18 -8.74228e-08L13 -3.0598e-07C11.9 -3.54062e-07 11 0.9 11 2L11 14C11 15.1 11.9 16 13 16L18 16C19.1 16 20 15.1 20 14ZM7 14L2 14L2 2L7 2L7 14ZM9 14L9 2C9 0.9 8.1 -5.20166e-07 7 -5.68248e-07L2 -7.86805e-07C0.9 -8.34888e-07 -3.93403e-08 0.9 -8.74228e-08 2L-6.11959e-07 14C-6.60042e-07 15.1 0.9 16 2 16L7 16C8.1 16 9 15.1 9 14Z"/> </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml new file mode 100644 index 000000000000..e307f007e4a4 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="210.0dp" + android:height="64.0dp" + android:tint="@color/decor_button_light_color" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="8.0" + android:translateY="8.0" > + <path + android:fillColor="@android:color/white" + android:pathData="M18.3334 14L13.3334 14L13.3334 2L18.3334 2L18.3334 14ZM20.3334 14L20.3334 2C20.3334 0.9 19.4334 -3.93402e-08 18.3334 -8.74228e-08L13.3334 -3.0598e-07C12.2334 -3.54062e-07 11.3334 0.9 11.3334 2L11.3334 14C11.3334 15.1 12.2334 16 13.3334 16L18.3334 16C19.4334 16 20.3334 15.1 20.3334 14ZM7.33337 14L2.33337 14L2.33337 2L7.33337 2L7.33337 14ZM9.33337 14L9.33337 2C9.33337 0.899999 8.43337 -5.20166e-07 7.33337 -5.68248e-07L2.33337 -7.86805e-07C1.23337 -8.34888e-07 0.333374 0.899999 0.333374 2L0.333373 14C0.333373 15.1 1.23337 16 2.33337 16L7.33337 16C8.43337 16 9.33337 15.1 9.33337 14Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml new file mode 100644 index 000000000000..d9a140b810f8 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> + <!-- + ~ 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. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout +xmlns:android="http://schemas.android.com/apk/res/android" +android:id="@+id/handle_menu" +android:layout_width="wrap_content" +android:layout_height="wrap_content" +android:gravity="center_horizontal" +android:background="@drawable/decor_caption_title"> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/fullscreen_button" + android:contentDescription="@string/fullscreen_text" + android:background="@drawable/caption_fullscreen_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/split_screen_button" + android:contentDescription="@string/split_screen_text" + android:background="@drawable/caption_split_screen_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/floating_button" + android:contentDescription="@string/float_button_text" + android:background="@drawable/caption_floating_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/desktop_button" + android:contentDescription="@string/desktop_text" + android:background="@drawable/caption_desktop_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/more_button" + android:contentDescription="@string/more_button_text" + android:background="@drawable/caption_more_button"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml index 38cd5702f134..51e634c17532 100644 --- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml +++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml @@ -19,14 +19,10 @@ android:id="@+id/caption" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center_horizontal" android:background="@drawable/decor_caption_title"> <Button + style="@style/CaptionButtonStyle" android:id="@+id/back_button" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_margin="5dp" - android:padding="4dp" android:contentDescription="@string/back_button_text" android:background="@drawable/decor_back_button_dark" /> @@ -39,11 +35,8 @@ android:contentDescription="@string/handle_text" android:background="@drawable/decor_handle_dark"/> <Button + style="@style/CaptionButtonStyle" android:id="@+id/close_window" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_margin="5dp" - android:padding="4dp" android:contentDescription="@string/close_button_text" android:background="@drawable/decor_close_button_dark"/> </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 4807f08b4bed..097a567c6b43 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -202,4 +202,14 @@ <string name="back_button_text">Back</string> <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] --> <string name="handle_text">Handle</string> + <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> + <string name="fullscreen_text">Fullscreen</string> + <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> + <string name="desktop_text">Desktop Mode</string> + <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> + <string name="split_screen_text">Split Screen</string> + <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> + <string name="more_button_text">More</string> + <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] --> + <string name="float_button_text">Float</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 19f7c3ef4364..a8597210d72e 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -30,6 +30,13 @@ <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item> </style> + <style name="CaptionButtonStyle"> + <item name="android:layout_width">32dp</item> + <item name="android:layout_height">32dp</item> + <item name="android:layout_margin">5dp</item> + <item name="android:padding">4dp</item> + </style> + <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">@dimen/split_divider_bar_width</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index dca516a327b0..36dd8edaa8b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,11 +26,16 @@ import android.app.ActivityTaskManager; import android.content.Context; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; +import android.view.InputChannel; import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -64,8 +69,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final SyncTransactionQueue mSyncQueue; private FreeformTaskTransitionStarter mTransitionStarter; private DesktopModeController mDesktopModeController; + private EventReceiver mEventReceiver; + private InputMonitor mInputMonitor; private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); public CaptionWindowDecorViewModel( Context context, @@ -108,12 +116,19 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration); + TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration, + mDragStartListener); CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragResizeCallback(taskPositioner); setupWindowDecorationForTransition(taskInfo, startT, finishT); + if (mInputMonitor == null) { + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "caption-touch", mContext.getDisplayId()); + mEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + } return true; } @@ -165,6 +180,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public void onClick(View v) { + CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (id == R.id.close_window) { WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -176,6 +192,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } else if (id == R.id.back_button) { injectBackKey(); + } else if (id == R.id.caption_handle) { + decoration.createHandleMenu(); + } else if (id == R.id.desktop_button) { + mDesktopModeController.setDesktopModeActive(true); + decoration.closeHandleMenu(); + } else if (id == R.id.fullscreen_button) { + mDesktopModeController.setDesktopModeActive(false); + decoration.closeHandleMenu(); + decoration.setButtonVisibility(); } } private void injectBackKey() { @@ -257,6 +282,36 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } + // InputEventReceiver to listen for touch input outside of caption bounds + private class EventReceiver extends InputEventReceiver { + EventReceiver(InputChannel channel, Looper looper) { + super(channel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = false; + if (event instanceof MotionEvent + && ((MotionEvent) event).getActionMasked() == MotionEvent.ACTION_UP) { + handled = true; + CaptionWindowDecorViewModel.this.handleMotionEvent((MotionEvent) event); + } + finishInputEvent(event, handled); + } + } + + // If any input received is outside of caption bounds, turn off handle menu + private void handleMotionEvent(MotionEvent ev) { + int size = mWindowDecorByTaskId.size(); + for (int i = 0; i < size; i++) { + CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration != null) { + decoration.closeHandleMenuIfNeeded(ev); + } + } + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; return DesktopModeStatus.IS_SUPPORTED @@ -264,4 +319,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { && mDisplayController.getDisplayContext(taskInfo.displayId) .getResources().getConfiguration().smallestScreenWidthDp >= 600; } + + private class DragStartListenerImpl implements TaskPositioner.DragStartListener{ + @Override + public void onDragStart(int taskId) { + mWindowDecorByTaskId.get(taskId).closeHandleMenu(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 9d61c14e1435..03cad043ed67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -20,10 +20,14 @@ import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.view.Choreographer; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; @@ -58,6 +62,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private boolean mDesktopActive; + private AdditionalWindow mHandleMenu; + CaptionWindowDecoration( Context context, DisplayController displayController, @@ -123,7 +129,20 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL if (isDragResizeable) { mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); } + final Resources resources = mDecorWindowContext.getResources(); + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final int captionHeight = loadDimensionPixelSize(resources, + mRelayoutParams.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, + mRelayoutParams.mCaptionWidthId); + final int captionLeft = taskBounds.width() / 2 + - captionWidth / 2; + final int captionTop = taskBounds.top + <= captionHeight / 2 ? 0 : -captionHeight / 2; + mRelayoutParams.setCaptionPosition(captionLeft, captionTop); + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + taskInfo = null; // Clear it just in case we use it accidentally mTaskOrganizer.applyTransaction(wct); @@ -137,15 +156,14 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } // If this task is not focused, do not show caption. - setCaptionVisibility(taskInfo.isFocused); + setCaptionVisibility(mTaskInfo.isFocused); // Only handle should show if Desktop Mode is inactive. boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext); - if (mDesktopActive != desktopCurrentStatus && taskInfo.isFocused) { + if (mDesktopActive != desktopCurrentStatus && mTaskInfo.isFocused) { mDesktopActive = desktopCurrentStatus; setButtonVisibility(); } - taskInfo = null; // Clear it just in case we use it accidentally if (!isDragResizeable) { closeDragResizeListener(); @@ -184,9 +202,22 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL back.setOnClickListener(mOnCaptionButtonClickListener); View handle = caption.findViewById(R.id.caption_handle); handle.setOnTouchListener(mOnCaptionTouchListener); + handle.setOnClickListener(mOnCaptionButtonClickListener); setButtonVisibility(); } + private void setupHandleMenu() { + View menu = mHandleMenu.mWindowViewHost.getView(); + View fullscreen = menu.findViewById(R.id.fullscreen_button); + fullscreen.setOnClickListener(mOnCaptionButtonClickListener); + View desktop = menu.findViewById(R.id.desktop_button); + desktop.setOnClickListener(mOnCaptionButtonClickListener); + View split = menu.findViewById(R.id.split_screen_button); + split.setOnClickListener(mOnCaptionButtonClickListener); + View more = menu.findViewById(R.id.more_button); + more.setOnClickListener(mOnCaptionButtonClickListener); + } + /** * Sets caption visibility based on task focus. * @@ -194,8 +225,9 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL */ private void setCaptionVisibility(boolean visible) { int v = visible ? View.VISIBLE : View.GONE; - View caption = mResult.mRootView.findViewById(R.id.caption); - caption.setVisibility(v); + View captionView = mResult.mRootView.findViewById(R.id.caption); + captionView.setVisibility(v); + if (!visible) closeHandleMenu(); } /** @@ -203,6 +235,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL * */ public void setButtonVisibility() { + mDesktopActive = DesktopModeStatus.isActive(mContext); int v = mDesktopActive ? View.VISIBLE : View.GONE; View caption = mResult.mRootView.findViewById(R.id.caption); View back = caption.findViewById(R.id.back_button); @@ -220,6 +253,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT); } + public boolean isHandleMenuActive() { + return mHandleMenu != null; + } + private void closeDragResizeListener() { if (mDragResizeListener == null) { return; @@ -228,9 +265,67 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; } + /** + * Create and display handle menu window + */ + public void createHandleMenu() { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final Resources resources = mDecorWindowContext.getResources(); + int x = mRelayoutParams.mCaptionX; + int y = mRelayoutParams.mCaptionY; + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + String namePrefix = "Caption Menu"; + mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t, + x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY, + width, height); + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + setupHandleMenu(); + } + + /** + * Close the handle menu window + */ + public void closeHandleMenu() { + if (!isHandleMenuActive()) return; + mHandleMenu.releaseView(); + mHandleMenu = null; + } + + @Override + void releaseViews() { + closeHandleMenu(); + super.releaseViews(); + } + + /** + * Close an open handle menu if input is outside of menu coordinates + * @param ev the tapped point to compare against + * @return + */ + public void closeHandleMenuIfNeeded(MotionEvent ev) { + if (mHandleMenu != null) { + Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) + .positionInParent; + final Resources resources = mDecorWindowContext.getResources(); + ev.offsetLocation(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); + ev.offsetLocation(-positionInParent.x, -positionInParent.y); + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + if (!(ev.getX() >= 0 && ev.getY() >= 0 + && ev.getX() <= width && ev.getY() <= height)) { + closeHandleMenu(); + } + } + } + @Override public void close() { closeDragResizeListener(); + closeHandleMenu(); super.close(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index 27c10114ac0e..f0f2db7ded80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -42,14 +42,18 @@ class TaskPositioner implements DragResizeCallback { private final Rect mResizeTaskBounds = new Rect(); private int mCtrlType; + private DragStartListener mDragStartListener; - TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) { + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DragStartListener dragStartListener) { mTaskOrganizer = taskOrganizer; mWindowDecoration = windowDecoration; + mDragStartListener = dragStartListener; } @Override public void onDragResizeStart(int ctrlType, float x, float y) { + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( @@ -97,4 +101,12 @@ class TaskPositioner implements DragResizeCallback { mTaskOrganizer.applyTransaction(wct); } } + + interface DragStartListener { + /** + * Inform the implementing class that a drag resize has started + * @param taskId id of this positioner's {@link WindowDecoration} + */ + void onDragStart(int taskId); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index b314163802ca..7ecb3f3f6355 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -200,16 +200,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); final Resources resources = mDecorWindowContext.getResources(); - final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); - final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); + outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() + loadDimensionPixelSize(resources, params.mOutsetRightId) - - decorContainerOffsetX; + - outResult.mDecorContainerOffsetX; outResult.mHeight = taskBounds.height() + loadDimensionPixelSize(resources, params.mOutsetBottomId) - - decorContainerOffsetY; + - outResult.mDecorContainerOffsetY; startT.setPosition( - mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) + mDecorationContainerSurface, + outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY) .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) // TODO(b/244455401): Change the z-order when it's better organized @@ -252,14 +253,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); - //Prevent caption from going offscreen if task is too high up - final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; - startT.setPosition( - mCaptionContainerSurface, -decorContainerOffsetX - + taskBounds.width() / 2 - captionWidth / 2, - -decorContainerOffsetY - captionYPos) - .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight) + mCaptionContainerSurface, + -outResult.mDecorContainerOffsetX + params.mCaptionX, + -outResult.mDecorContainerOffsetY + params.mCaptionY) + .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight) .show(mCaptionContainerSurface); if (mCaptionWindowManager == null) { @@ -292,7 +290,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption insets mCaptionInsetsRect.set(taskBounds); mCaptionInsetsRect.bottom = - mCaptionInsetsRect.top + captionHeight - captionYPos; + mCaptionInsetsRect.top + captionHeight + params.mCaptionY; wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); } else { @@ -302,10 +300,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Task surface itself Point taskPosition = mTaskInfo.positionInParent; mTaskSurfaceCrop.set( - decorContainerOffsetX, - decorContainerOffsetY, - outResult.mWidth + decorContainerOffsetX, - outResult.mHeight + decorContainerOffsetY); + outResult.mDecorContainerOffsetX, + outResult.mDecorContainerOffsetY, + outResult.mWidth + outResult.mDecorContainerOffsetX, + outResult.mHeight + outResult.mDecorContainerOffsetY); startT.show(mTaskSurface); finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) .setCrop(mTaskSurface, mTaskSurfaceCrop); @@ -326,7 +324,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return true; } - private void releaseViews() { + void releaseViews() { if (mViewHost != null) { mViewHost.release(); mViewHost = null; @@ -369,20 +367,60 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } - private static int loadDimensionPixelSize(Resources resources, int resourceId) { + static int loadDimensionPixelSize(Resources resources, int resourceId) { if (resourceId == Resources.ID_NULL) { return 0; } return resources.getDimensionPixelSize(resourceId); } - private static float loadDimension(Resources resources, int resourceId) { + static float loadDimension(Resources resources, int resourceId) { if (resourceId == Resources.ID_NULL) { return 0; } return resources.getDimension(resourceId); } + /** + * Create a window associated with this WindowDecoration. + * Note that subclass must dispose of this when the task is hidden/closed. + * @param layoutId layout to make the window from + * @param t the transaction to apply + * @param xPos x position of new window + * @param yPos y position of new window + * @param width width of new window + * @param height height of new window + * @return + */ + AdditionalWindow addWindow(int layoutId, String namePrefix, + SurfaceControl.Transaction t, int xPos, int yPos, int width, int height) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + SurfaceControl windowSurfaceControl = builder + .setName(namePrefix + " of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setParent(mDecorationContainerSurface) + .build(); + View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); + + t.setPosition( + windowSurfaceControl, xPos, yPos) + .setWindowCrop(windowSurfaceControl, width, height) + .show(windowSurfaceControl); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(width, height, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, + windowSurfaceControl, null /* hostInputToken */); + SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory + .create(mDecorWindowContext, mDisplay, windowManager); + viewHost.setView(v, lp); + return new AdditionalWindow(windowSurfaceControl, viewHost, + mSurfaceControlTransactionSupplier); + } + static class RelayoutParams{ RunningTaskInfo mRunningTaskInfo; int mLayoutResId; @@ -395,6 +433,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mOutsetLeftId; int mOutsetRightId; + int mCaptionX; + int mCaptionY; + void setOutsets(int leftId, int topId, int rightId, int bottomId) { mOutsetLeftId = leftId; mOutsetTopId = topId; @@ -402,6 +443,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetBottomId = bottomId; } + void setCaptionPosition(int left, int top) { + mCaptionX = left; + mCaptionY = top; + } + void reset() { mLayoutResId = Resources.ID_NULL; mCaptionHeightId = Resources.ID_NULL; @@ -412,6 +458,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetBottomId = Resources.ID_NULL; mOutsetLeftId = Resources.ID_NULL; mOutsetRightId = Resources.ID_NULL; + + mCaptionX = 0; + mCaptionY = 0; } } @@ -419,10 +468,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mWidth; int mHeight; T mRootView; + int mDecorContainerOffsetX; + int mDecorContainerOffsetY; void reset() { mWidth = 0; mHeight = 0; + mDecorContainerOffsetX = 0; + mDecorContainerOffsetY = 0; mRootView = null; } } @@ -432,4 +485,41 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return new SurfaceControlViewHost(c, d, wmm); } } + + /** + * Subclass for additional windows associated with this WindowDecoration + */ + static class AdditionalWindow { + SurfaceControl mWindowSurface; + SurfaceControlViewHost mWindowViewHost; + Supplier<SurfaceControl.Transaction> mTransactionSupplier; + + private AdditionalWindow(SurfaceControl surfaceControl, + SurfaceControlViewHost surfaceControlViewHost, + Supplier<SurfaceControl.Transaction> transactionSupplier) { + mWindowSurface = surfaceControl; + mWindowViewHost = surfaceControlViewHost; + mTransactionSupplier = transactionSupplier; + } + + void releaseView() { + WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM(); + + if (mWindowViewHost != null) { + mWindowViewHost.release(); + mWindowViewHost = null; + } + windowManager = null; + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + boolean released = false; + if (mWindowSurface != null) { + t.remove(mWindowSurface); + mWindowSurface = null; + released = true; + } + if (released) { + t.apply(); + } + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 79978929cf3e..651d9356d9ba 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -21,6 +21,7 @@ package com.android.wm.shell.flicker import com.android.server.wm.traces.common.ComponentNameMatcher const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" +const val LAUNCHER_UI_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" val APP_PAIR_SPLIT_DIVIDER_COMPONENT = ComponentNameMatcher("", "AppPairSplitDivider#") val DOCKED_STACK_DIVIDER_COMPONENT = ComponentNameMatcher("", "DockedStackDivider#") val SPLIT_SCREEN_DIVIDER_COMPONENT = ComponentNameMatcher("", "StageCoordinatorSplitDivider#") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index f8025396ddc9..7546a55c08fa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -80,7 +80,7 @@ class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest( transitions { tapl.goHome() } } - @FlakyTest + @FlakyTest(bugId = 256863309) @Test override fun pipLayerReduces() { testSpec.assertLayers { @@ -108,14 +108,6 @@ class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest( } } - @FlakyTest(bugId = 239807171) - @Test - override fun pipAppLayerAlwaysVisible() = super.pipAppLayerAlwaysVisible() - - @FlakyTest(bugId = 239807171) - @Test - override fun pipLayerRemainInsideVisibleBounds() = super.pipLayerRemainInsideVisibleBounds() - @Presubmit @Test override fun focusChanges() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 9e765752ea9e..2bce8e45c553 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -53,7 +53,7 @@ class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testS override val transition: FlickerBuilder.() -> Unit get() = { super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, textEditApp) } + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) } transitions { SplitScreenUtils.copyContentInSplit( instrumentation, diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index 45eae2e2fe40..475749834711 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -55,7 +55,7 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } transitions { if (tapl.isTablet) { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt index 6cfbb4789dc1..1d61955bc0a8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -52,7 +52,7 @@ class DismissSplitScreenByGoHome( get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } transitions { tapl.goHome() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt index a80c88aad3d9..8d771fe3a1ff 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -56,7 +56,7 @@ class DragDividerToResize(testSpec: FlickerTestParameter) : SplitScreenBase(test get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt index 936afa9801b7..fb7b8b7926e3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -34,7 +34,6 @@ import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible import com.android.wm.shell.flicker.splitScreenEntered import org.junit.Assume -import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +54,6 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen get() = { super.transition(this) setup { - tapl.workspace.switchToOverview().dismissAllTasks() primaryApp.launchViaIntent(wmHelper) secondaryApp.launchViaIntent(wmHelper) tapl.goHome() @@ -65,7 +63,7 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen .waitForAndVerify() } transitions { - SplitScreenUtils.splitFromOverview(tapl) + SplitScreenUtils.splitFromOverview(tapl, device) SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt index e6d6379e750c..c8413337a1e6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt @@ -34,6 +34,7 @@ abstract class SplitScreenBase(testSpec: FlickerTestParameter) : BaseTest(testSp tapl.setEnableRotation(true) setRotation(testSpec.startRotation) tapl.setExpectedRotation(testSpec.startRotation) + tapl.workspace.switchToOverview().dismissAllTasks() } teardown { primaryApp.exit(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt index 6453ed869681..ead451f07653 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt @@ -25,6 +25,7 @@ import android.view.ViewConfiguration import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.ImeAppHelper @@ -38,13 +39,16 @@ import com.android.server.wm.traces.common.IComponentMatcher import com.android.server.wm.traces.common.IComponentNameMatcher import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import java.util.Collections internal object SplitScreenUtils { private const val TIMEOUT_MS = 3_000L private const val DRAG_DURATION_MS = 1_000L private const val NOTIFICATION_SCROLLER = "notification_stack_scroller" private const val DIVIDER_BAR = "docked_divider_handle" + private const val OVERVIEW_SNAPSHOT = "snapshot" private const val GESTURE_STEP_MS = 16L private const val LONG_PRESS_TIME_MS = 100L private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") @@ -55,6 +59,8 @@ internal object SplitScreenUtils { get() = By.text("Flicker Test Notification") private val dividerBarSelector: BySelector get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) + private val overviewSnapshotSelector: BySelector + get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT) fun getPrimary(instrumentation: Instrumentation): StandardAppHelper = SimpleAppHelper( @@ -94,24 +100,39 @@ internal object SplitScreenUtils { fun enterSplit( wmHelper: WindowManagerStateHelper, tapl: LauncherInstrumentation, + device: UiDevice, primaryApp: StandardAppHelper, secondaryApp: StandardAppHelper ) { - tapl.workspace.switchToOverview().dismissAllTasks() primaryApp.launchViaIntent(wmHelper) secondaryApp.launchViaIntent(wmHelper) tapl.goHome() wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() - splitFromOverview(tapl) + splitFromOverview(tapl, device) waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } - fun splitFromOverview(tapl: LauncherInstrumentation) { + fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) { // Note: The initial split position in landscape is different between tablet and phone. // In landscape, tablet will let the first app split to right side, and phone will // split to left side. if (tapl.isTablet) { - tapl.workspace.switchToOverview().overviewActions.clickSplit().currentTask.open() + // TAPL's currentTask on tablet is sometimes not what we expected if the overview + // contains more than 3 task views. We need to use uiautomator directly to find the + // second task to split. + tapl.workspace.switchToOverview().overviewActions.clickSplit() + val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS) + if (snapshots == null || snapshots.size < 1) { + error("Fail to find a overview snapshot to split.") + } + + // Find the second task in the upper right corner in split select mode by sorting + // 'left' in descending order and 'top' in ascending order. + Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 -> + t2.getVisibleBounds().left - t1.getVisibleBounds().left}) + Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 -> + t1.getVisibleBounds().top - t2.getVisibleBounds().top}) + snapshots[0].click() } else { tapl.workspace .switchToOverview() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index ad7a531b589d..f7610c48a0f8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -56,7 +56,7 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } transitions { SplitScreenUtils.doubleTapDividerToSwitch(device) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt index 553840cf0e47..993dba28bbc4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -52,7 +52,7 @@ class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScr get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) thirdApp.launchViaIntent(wmHelper) wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index 1f117d0cbd94..2a552cdd67e8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -51,7 +51,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) tapl.goHome() wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt index d7b3ec2256c1..7f81baef315b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -51,7 +51,7 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB get() = { super.transition(this) setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) tapl.goHome() wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt new file mode 100644 index 000000000000..d84954dcdd09 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt @@ -0,0 +1,248 @@ +/* + * 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.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowBecomesInvisible +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.layerBecomesInvisible +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch between two split pairs. + * + * To run this test: `atest WMShellFlickerTests:SwitchBetweenSplitPairs` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SwitchBetweenSplitPairs(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + private val thirdApp = SplitScreenUtils.getIme(instrumentation) + private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation) + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp) + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + teardown { + thirdApp.exit(wmHelper) + fourthApp.exit(wmHelper) + } + } + + @Postsubmit + @Test + fun cujCompleted() { + testSpec.appWindowIsVisibleAtStart(thirdApp) + testSpec.appWindowIsVisibleAtStart(fourthApp) + testSpec.splitScreenDividerIsVisibleAtStart() + + testSpec.appWindowIsVisibleAtEnd(primaryApp) + testSpec.appWindowIsVisibleAtEnd(secondaryApp) + testSpec.appWindowIsInvisibleAtEnd(thirdApp) + testSpec.appWindowIsInvisibleAtEnd(fourthApp) + testSpec.splitScreenDividerIsVisibleAtEnd() + } + + @Postsubmit + @Test + fun splitScreenDividerInvisibleAtMiddle() = + testSpec.assertLayers { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } + + @FlakyTest(bugId = 247095572) + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @FlakyTest(bugId = 247095572) + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @FlakyTest(bugId = 247095572) + @Test + fun thirdAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(thirdApp) + + @FlakyTest(bugId = 247095572) + @Test + fun fourthAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(fourthApp) + + @Postsubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Postsubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Postsubmit + @Test + fun thirdAppBoundsIsVisibleAtBegin() = + testSpec.assertLayersStart { + this.splitAppLayerBoundsSnapToDivider( + thirdApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false, + testSpec.startRotation + ) + } + + @Postsubmit + @Test + fun fourthAppBoundsIsVisibleAtBegin() = + testSpec.assertLayersStart { + this.splitAppLayerBoundsSnapToDivider( + fourthApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true, + testSpec.startRotation + ) + } + + @Postsubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @Postsubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + @Postsubmit + @Test + fun thirdAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(thirdApp) + + @Postsubmit + @Test + fun fourthAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(fourthApp) + + /** {@inheritDoc} */ + @FlakyTest(bugId = 251268711) + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 4d37e5dbc4dc..15181b1549f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.content.Context; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; @@ -64,6 +65,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; @@ -102,12 +104,14 @@ public class WindowDecorationTests extends ShellTestCase { private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); private SurfaceControl.Transaction mMockSurfaceControlStartT; private SurfaceControl.Transaction mMockSurfaceControlFinishT; + private SurfaceControl.Transaction mMockSurfaceControlAddWindowT; private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); @Before public void setUp() { mMockSurfaceControlStartT = createMockSurfaceControlTransaction(); mMockSurfaceControlFinishT = createMockSurfaceControlTransaction(); + mMockSurfaceControlAddWindowT = createMockSurfaceControlTransaction(); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) .create(any(), any(), any()); @@ -227,8 +231,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8); - verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); + verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); @@ -242,7 +246,7 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 332), + new Rect(100, 300, 400, 364), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } @@ -366,6 +370,71 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlViewHost).setView(same(mMockView), any()); } + @Test + public void testAddWindow() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final SurfaceControl decorContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder decorContainerSurfaceBuilder = + createMockSurfaceControlBuilder(decorContainerSurface); + mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); + final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder taskBackgroundSurfaceBuilder = + createMockSurfaceControlBuilder(taskBackgroundSurface); + mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder); + final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder captionContainerSurfaceBuilder = + createMockSurfaceControlBuilder(captionContainerSurface); + mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); + + final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mMockSurfaceControlTransactions.add(t); + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setBounds(TASK_BOUNDS) + .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) + .setVisible(true) + .build(); + taskInfo.isFocused = true; + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + windowDecor.relayout(taskInfo); + + final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder additionalWindowSurfaceBuilder = + createMockSurfaceControlBuilder(additionalWindowSurface); + mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder); + + WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow(); + + verify(additionalWindowSurfaceBuilder).setContainerLayer(); + verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); + verify(additionalWindowSurfaceBuilder).build(); + verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40); + verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 432, 64); + verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); + verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) + .create(any(), eq(defaultDisplay), any()); + assertThat(additionalWindow.mWindowViewHost).isNotNull(); + + additionalWindow.releaseView(); + + assertThat(additionalWindow.mWindowViewHost).isNull(); + assertThat(additionalWindow.mWindowSurface).isNull(); + } + private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), @@ -429,5 +498,20 @@ public class WindowDecorationTests extends ShellTestCase { relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); } + + private WindowDecoration.AdditionalWindow addTestWindow() { + final Resources resources = mDecorWindowContext.getResources(); + int x = mRelayoutParams.mCaptionX; + int y = mRelayoutParams.mCaptionY; + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + String name = "Test Window"; + WindowDecoration.AdditionalWindow additionalWindow = + addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT, + x - mRelayoutResult.mDecorContainerOffsetX, + y - mRelayoutResult.mDecorContainerOffsetY, + width, height); + return additionalWindow; + } } } diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp index 6990ad0fbd7d..62ffe3898667 100644 --- a/packages/CarrierDefaultApp/Android.bp +++ b/packages/CarrierDefaultApp/Android.bp @@ -13,4 +13,9 @@ android_app { libs: ["SliceStore"], platform_apis: true, certificate: "platform", + optimize: { + proguard_flags_files: [ + "proguard.flags", + ], + }, } diff --git a/packages/CarrierDefaultApp/assets/slice_store_test.html b/packages/CarrierDefaultApp/assets/slice_store_test.html new file mode 100644 index 000000000000..7ddbd2d5f245 --- /dev/null +++ b/packages/CarrierDefaultApp/assets/slice_store_test.html @@ -0,0 +1,78 @@ +<!-- + ~ 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. + --> + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="description" content=" + This is a HTML page that calls and verifies responses from the @JavascriptInterface functions of + SliceStoreWebInterface. Test SliceStore APIs using ADB shell commands and the APIs below: + + FROM TERMINAL: + Allow device to override carrier configs: + $ adb root + Set PREMIUM_CAPABILITY_PRIORITIZE_LATENCY enabled: + $ adb shell cmd phone cc set-value -p supported_premium_capabilities_int_array 34 + Set the carrier purchase URL to this test HTML file: + $ adb shell cmd phone cc set-value -p premium_capability_purchase_url_string \ + file:///android_asset/slice_store_test.html + OPTIONAL: Allow premium capability purchase on LTE: + $ adb shell cmd phone cc set-value -p premium_capability_supported_on_lte_bool true + OPTIONAL: Override ServiceState to fake a NR SA connection: + $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --ei data_rat 20 + + FROM TEST ACTIVITY: + TelephonyManager tm = getApplicationContext().getSystemService(TelephonyManager.class) + tm.isPremiumCapabilityAvailable(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY); + LinkedBlockingQueue<Integer> purchaseRequests = new LinkedBlockingQueue<>(); + tm.purchasePremiumCapability(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, + this.getMainExecutor(), request::offer); + + When the test application starts, this HTML will be loaded into the WebView along with the + associated JavaScript functions in file:///android_asset/slice_store_test.js. + Click on the buttons in the HTML to call the corresponding @JavascriptInterface APIs. + + RESET DEVICE STATE: + Clear carrier configurations that were set: + $ adb shell cmd phone cc clear-values + Clear ServiceState override that was set: + $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --es action reset + "> + <title>Test SliceStoreActivity</title> + <script type="text/javascript" src="slice_store_test.js"></script> +</head> +<body> + <h1>Test SliceStoreActivity</h1> + <h2>Get requested premium capability</h2> + <button type="button" onclick="testGetRequestedCapability()"> + Get requested premium capability + </button> + <p id="requested_capability"></p> + + <h2>Notify purchase successful</h2> + <button type="button" onclick="testNotifyPurchaseSuccessful(60000)"> + Notify purchase successful for 1 minute + </button> + <p id="purchase_successful"></p> + + <h2>Notify purchase failed</h2> + <button type="button" onclick="testNotifyPurchaseFailed()"> + Notify purchase failed + </button> + <p id="purchase_failed"></p> +</body> +</html> diff --git a/packages/CarrierDefaultApp/assets/slice_store_test.js b/packages/CarrierDefaultApp/assets/slice_store_test.js new file mode 100644 index 000000000000..f12a6daf8de3 --- /dev/null +++ b/packages/CarrierDefaultApp/assets/slice_store_test.js @@ -0,0 +1,33 @@ +/* + * 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. + */ + +function testGetRequestedCapability() { + let capability = SliceStoreWebInterface.getRequestedCapability(); + document.getElementById("requested_capability").innerHTML = + "Premium capability requested: " + capability; +} + +function testNotifyPurchaseSuccessful(duration_ms_long = 0) { + SliceStoreWebInterface.notifyPurchaseSuccessful(duration); + document.getElementById("purchase_successful").innerHTML = + "Notified purchase success for duration: " + duration; +} + +function testNotifyPurchaseFailed() { + SliceStoreWebInterface.notifyPurchaseFailed(); + document.getElementById("purchase_failed").innerHTML = + "Notified purchase failed."; +} diff --git a/packages/CarrierDefaultApp/proguard.flags b/packages/CarrierDefaultApp/proguard.flags new file mode 100644 index 000000000000..64fec2ccbef2 --- /dev/null +++ b/packages/CarrierDefaultApp/proguard.flags @@ -0,0 +1,4 @@ +# Keep classes and methods that have the @JavascriptInterface annotation +-keepclassmembers class * { + @android.webkit.JavascriptInterface <methods>; +} diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java index 602e31cbd130..348e3895690c 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java @@ -20,47 +20,63 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.NotificationManager; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; +import android.view.KeyEvent; import android.webkit.WebView; import com.android.phone.slicestore.SliceStore; import java.net.MalformedURLException; import java.net.URL; +import java.util.concurrent.TimeUnit; /** * Activity that launches when the user clicks on the network boost notification. + * This will open a {@link WebView} for the carrier website to allow the user to complete the + * premium capability purchase. + * The carrier website can get the requested premium capability using the JavaScript interface + * method {@code SliceStoreWebInterface.getRequestedCapability()}. + * If the purchase is successful, the carrier website shall notify SliceStore using the JavaScript + * interface method {@code SliceStoreWebInterface.notifyPurchaseSuccessful(duration)}, where + * {@code duration} is the duration of the network boost. + * If the purchase was not successful, the carrier website shall notify SliceStore using the + * JavaScript interface method {@code SliceStoreWebInterface.notifyPurchaseFailed()}. + * If either of these notification methods are not called, the purchase cannot be completed + * successfully and the purchase request will eventually time out. */ public class SliceStoreActivity extends Activity { private static final String TAG = "SliceStoreActivity"; - private URL mUrl; - private WebView mWebView; - private int mPhoneId; + private @NonNull WebView mWebView; + private @NonNull Context mApplicationContext; private int mSubId; - private @TelephonyManager.PremiumCapability int mCapability; + @TelephonyManager.PremiumCapability protected int mCapability; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); - mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID, - SubscriptionManager.INVALID_PHONE_INDEX); mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, SliceStore.PREMIUM_CAPABILITY_INVALID); - mUrl = getUrl(); - logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability=" + mApplicationContext = getApplicationContext(); + URL url = getUrl(); + logd("onCreate: subId=" + mSubId + ", capability=" + TelephonyManager.convertPremiumCapabilityToString(mCapability) - + ", mUrl=" + mUrl); - getApplicationContext().getSystemService(NotificationManager.class) + + ", url=" + url); + + // Cancel network boost notification + mApplicationContext.getSystemService(NotificationManager.class) .cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability); + + // Verify intent and values are valid if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) { loge("Not starting SliceStoreActivity with an invalid Intent: " + intent); SliceStoreBroadcastReceiver.sendSliceStoreResponse( @@ -68,10 +84,15 @@ public class SliceStoreActivity extends Activity { finishAndRemoveTask(); return; } - if (mUrl == null) { - loge("Unable to create a URL from carrier configs."); - SliceStoreBroadcastReceiver.sendSliceStoreResponse( - intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR); + if (url == null) { + String error = "Unable to create a URL from carrier configs."; + loge(error); + Intent data = new Intent(); + data.putExtra(SliceStore.EXTRA_FAILURE_CODE, + SliceStore.FAILURE_CODE_CARRIER_URL_UNAVAILABLE); + data.putExtra(SliceStore.EXTRA_FAILURE_REASON, error); + SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData( + mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_CARRIER_ERROR, data); finishAndRemoveTask(); return; } @@ -83,12 +104,53 @@ public class SliceStoreActivity extends Activity { return; } + // Create a reference to this activity in SliceStoreBroadcastReceiver SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this); + // Create and configure WebView mWebView = new WebView(this); + // Enable JavaScript for the carrier purchase website to send results back to SliceStore + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.addJavascriptInterface(new SliceStoreWebInterface(this), "SliceStoreWebInterface"); + + // Display WebView setContentView(mWebView); - mWebView.loadUrl(mUrl.toString()); - // TODO(b/245882601): Get back response from WebView + mWebView.loadUrl(url.toString()); + } + + protected void onPurchaseSuccessful(long duration) { + logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium " + + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability) + + " for " + TimeUnit.MILLISECONDS.toMinutes(duration) + " minutes."); + Intent intent = new Intent(); + intent.putExtra(SliceStore.EXTRA_PURCHASE_DURATION, duration); + SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData( + mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_SUCCESS, intent); + finishAndRemoveTask(); + } + + protected void onPurchaseFailed(@SliceStore.FailureCode int failureCode, + @Nullable String failureReason) { + logd("onPurchaseFailed: Carrier website indicated purchase failed for premium capability " + + TelephonyManager.convertPremiumCapabilityToString(mCapability) + " with code: " + + SliceStore.convertFailureCodeToString(failureCode) + " and reason: " + + failureReason); + Intent data = new Intent(); + data.putExtra(SliceStore.EXTRA_FAILURE_CODE, failureCode); + data.putExtra(SliceStore.EXTRA_FAILURE_REASON, failureReason); + SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData( + mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_CARRIER_ERROR, data); + finishAndRemoveTask(); + } + + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + // Pressing back in the WebView will go to the previous page instead of closing SliceStore. + if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) { + mWebView.goBack(); + return true; + } + return super.onKeyDown(keyCode, event); } @Override @@ -100,8 +162,8 @@ public class SliceStoreActivity extends Activity { super.onDestroy(); } - private @Nullable URL getUrl() { - String url = getApplicationContext().getSystemService(CarrierConfigManager.class) + @Nullable private URL getUrl() { + String url = mApplicationContext.getSystemService(CarrierConfigManager.class) .getConfigForSubId(mSubId).getString( CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING); try { diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java index 7eb851dcdd58..7867ef1c6516 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.os.UserHandle; +import android.telephony.AnomalyReporter; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; @@ -37,6 +38,7 @@ import com.android.phone.slicestore.SliceStore; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the @@ -47,6 +49,12 @@ import java.util.Map; public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ private static final String TAG = "SliceStoreBroadcastReceiver"; + /** + * UUID to report an anomaly when receiving a PendingIntent from an application or process + * other than the Phone process. + */ + private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53"; + /** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */ private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities = new HashMap<>(); @@ -102,6 +110,28 @@ public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ } /** + * Send the PendingIntent containing the corresponding SliceStore response with additional data. + * + * @param context The Context to use to send the PendingIntent. + * @param intent The Intent containing the PendingIntent extra. + * @param extra The extra to get the PendingIntent to send. + * @param data The Intent containing additional data to send with the PendingIntent. + */ + public static void sendSliceStoreResponseWithData(@NonNull Context context, + @NonNull Intent intent, @NonNull String extra, @NonNull Intent data) { + PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class); + if (pendingIntent == null) { + loge("PendingIntent does not exist for extra: " + extra); + return; + } + try { + pendingIntent.send(context, 0 /* unused */, data); + } catch (PendingIntent.CanceledException e) { + loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e); + } + } + + /** * Check whether the Intent is valid and can be used to complete purchases in the SliceStore. * This checks that all necessary extras exist and that the values are valid. * @@ -139,7 +169,8 @@ public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED) && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR) && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED) - && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA); + && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA) + && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_SUCCESS); } private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) { @@ -148,12 +179,20 @@ public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ if (pendingIntent == null) { loge("isPendingIntentValid: " + intentType + " intent not found."); return false; - } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) { - return true; } - loge("isPendingIntentValid: " + intentType + " intent was created by " - + pendingIntent.getCreatorPackage() + " instead of the phone process."); - return false; + String creatorPackage = pendingIntent.getCreatorPackage(); + if (!creatorPackage.equals(TelephonyManager.PHONE_PROCESS_NAME)) { + String logStr = "isPendingIntentValid: " + intentType + " intent was created by " + + creatorPackage + " instead of the phone process."; + loge(logStr); + AnomalyReporter.reportAnomaly(UUID.fromString(UUID_BAD_PENDING_INTENT), logStr); + return false; + } + if (!pendingIntent.isBroadcast()) { + loge("isPendingIntentValid: " + intentType + " intent is not a broadcast."); + return false; + } + return true; } @NonNull private static String getPendingIntentType(@NonNull String extra) { @@ -162,6 +201,7 @@ public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error"; case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed"; case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data"; + case SliceStore.EXTRA_INTENT_SUCCESS: return "success"; default: { loge("Unknown pending intent extra: " + extra); return "unknown(" + extra + ")"; @@ -292,7 +332,6 @@ public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ logd("Closing SliceStore WebView since the user did not complete the purchase " + "in time."); sSliceStoreActivities.get(capability).get().finishAndRemoveTask(); - // TODO: Display a toast to indicate timeout for better UX? } } diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java new file mode 100644 index 000000000000..ab5d0809a1f6 --- /dev/null +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java @@ -0,0 +1,90 @@ +/* + * 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.carrierdefaultapp; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.telephony.TelephonyManager; +import android.webkit.JavascriptInterface; + +import com.android.phone.slicestore.SliceStore; + +/** + * SliceStore web interface class allowing carrier websites to send responses back to SliceStore + * using JavaScript. + */ +public class SliceStoreWebInterface { + @NonNull SliceStoreActivity mActivity; + + public SliceStoreWebInterface(@NonNull SliceStoreActivity activity) { + mActivity = activity; + } + /** + * Interface method allowing the carrier website to get the premium capability + * that was requested to purchase. + * + * This can be called using the JavaScript below: + * <script type="text/javascript"> + * function getRequestedCapability(duration) { + * SliceStoreWebInterface.getRequestedCapability(); + * } + * </script> + */ + @JavascriptInterface + @TelephonyManager.PremiumCapability public int getRequestedCapability() { + return mActivity.mCapability; + } + + /** + * Interface method allowing the carrier website to notify the SliceStore of a successful + * premium capability purchase and the duration for which the premium capability is purchased. + * + * This can be called using the JavaScript below: + * <script type="text/javascript"> + * function notifyPurchaseSuccessful(duration_ms_long = 0) { + * SliceStoreWebInterface.notifyPurchaseSuccessful(duration_ms_long); + * } + * </script> + * + * @param duration The duration for which the premium capability is purchased in milliseconds. + */ + @JavascriptInterface + public void notifyPurchaseSuccessful(long duration) { + mActivity.onPurchaseSuccessful(duration); + } + + /** + * Interface method allowing the carrier website to notify the SliceStore of a failed + * premium capability purchase. + * + * This can be called using the JavaScript below: + * <script type="text/javascript"> + * function notifyPurchaseFailed() { + * SliceStoreWebInterface.notifyPurchaseFailed(); + * } + * </script> + * + * @param failureCode The failure code. + * @param failureReason If the failure code is {@link SliceStore#FAILURE_CODE_UNKNOWN}, + * the human-readable reason for failure. + */ + @JavascriptInterface + public void notifyPurchaseFailed(@SliceStore.FailureCode int failureCode, + @Nullable String failureReason) { + mActivity.onPurchaseFailed(failureCode, failureReason); + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 0988cba2f424..01348e47932c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -23,6 +23,8 @@ import android.content.Intent import android.credentials.CreateCredentialRequest import android.credentials.ui.Constants import android.credentials.ui.Entry +import android.credentials.ui.CreateCredentialProviderData +import android.credentials.ui.GetCredentialProviderData import android.credentials.ui.ProviderData import android.credentials.ui.RequestInfo import android.credentials.ui.BaseDialogResult @@ -54,10 +56,22 @@ class CredentialManagerRepo( RequestInfo::class.java ) ?: testRequestInfo() - providerList = intent.extras?.getParcelableArrayList( - ProviderData.EXTRA_PROVIDER_DATA_LIST, - ProviderData::class.java - ) ?: testProviderList() + providerList = when (requestInfo.type) { + RequestInfo.TYPE_CREATE -> + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + CreateCredentialProviderData::class.java + ) ?: testCreateCredentialProviderList() + RequestInfo.TYPE_GET -> + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + GetCredentialProviderData::class.java + ) ?: testGetCredentialProviderList() + else -> { + // TODO: fail gracefully + throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") + } + } resultReceiver = intent.getParcelableExtra( Constants.EXTRA_RESULT_RECEIVER, @@ -84,7 +98,9 @@ class CredentialManagerRepo( } fun getCredentialInitialUiState(): GetCredentialUiState { - val providerList = GetFlowUtils.toProviderList(providerList, context) + val providerList = GetFlowUtils.toProviderList( + // TODO: handle runtime cast error + providerList as List<GetCredentialProviderData>, context) // TODO: covert from real requestInfo val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo( "Elisa Beckett", @@ -100,7 +116,9 @@ class CredentialManagerRepo( } fun createPasskeyInitialUiState(): CreatePasskeyUiState { - val providerList = CreateFlowUtils.toProviderList(providerList, context) + val providerList = CreateFlowUtils.toProviderList( + // Handle runtime cast error + providerList as List<CreateCredentialProviderData>, context) // TODO: covert from real requestInfo val requestDisplayInfo = RequestDisplayInfo( "Elisa Beckett", @@ -130,31 +148,64 @@ class CredentialManagerRepo( } // TODO: below are prototype functionalities. To be removed for productionization. - private fun testProviderList(): List<ProviderData> { + private fun testCreateCredentialProviderList(): List<CreateCredentialProviderData> { return listOf( - ProviderData.Builder( - "com.google", - "Google Password Manager", - Icon.createWithResource(context, R.drawable.ic_launcher_foreground)) - .setCredentialEntries( + CreateCredentialProviderData.Builder("com.google/com.google.CredentialManagerService") + .setSaveEntries( listOf<Entry>( newEntry("key1", "subkey-1", "elisa.beckett@gmail.com", "Elisa Backett", "20 passwords and 7 passkeys saved"), newEntry("key1", "subkey-2", "elisa.work@google.com", "Elisa Backett Work", "20 passwords and 7 passkeys saved"), ) - ).setActionChips( + ) + .setActionChips( listOf<Entry>( newEntry("key2", "subkey-1", "Go to Settings", "", "20 passwords and 7 passkeys saved"), newEntry("key2", "subkey-2", "Switch Account", "", "20 passwords and 7 passkeys saved"), ), + ) + .setIsDefaultProvider(true) + .build(), + CreateCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService") + .setSaveEntries( + listOf<Entry>( + newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", + "Elisa Backett", "20 passwords and 7 passkeys saved"), + newEntry("key1", "subkey-4", "elisa.work@dashlane.com", + "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + ) + ).setActionChips( + listOf<Entry>( + newEntry("key2", "subkey-3", "Manage Accounts", + "Manage your accounts in the dashlane app", + "20 passwords and 7 passkeys saved"), + ), ).build(), - ProviderData.Builder( - "com.dashlane", - "Dashlane", - Icon.createWithResource(context, R.drawable.ic_launcher_foreground)) + ) + } + + private fun testGetCredentialProviderList(): List<GetCredentialProviderData> { + return listOf( + GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService") + .setCredentialEntries( + listOf<Entry>( + newEntry("key1", "subkey-1", "elisa.beckett@gmail.com", + "Elisa Backett", "20 passwords and 7 passkeys saved"), + newEntry("key1", "subkey-2", "elisa.work@google.com", + "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + ) + ).setActionChips( + listOf<Entry>( + newEntry("key2", "subkey-1", "Go to Settings", "", + "20 passwords and 7 passkeys saved"), + newEntry("key2", "subkey-2", "Switch Account", "", + "20 passwords and 7 passkeys saved"), + ), + ).build(), + GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService") .setCredentialEntries( listOf<Entry>( newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", @@ -166,7 +217,7 @@ class CredentialManagerRepo( listOf<Entry>( newEntry("key2", "subkey-3", "Manage Accounts", "Manage your accounts in the dashlane app", - "20 passwords and 7 passkeys saved"), + "20 passwords and 7 passkeys saved"), ), ).build(), ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 2ba8748c16b7..bf0dba23cb64 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -18,7 +18,8 @@ package com.android.credentialmanager import android.content.Context import android.credentials.ui.Entry -import android.credentials.ui.ProviderData +import android.credentials.ui.GetCredentialProviderData +import android.credentials.ui.CreateCredentialProviderData import com.android.credentialmanager.createflow.CreateOptionInfo import com.android.credentialmanager.getflow.CredentialOptionInfo import com.android.credentialmanager.getflow.ProviderInfo @@ -28,7 +29,7 @@ class GetFlowUtils { companion object { fun toProviderList( - providerDataList: List<ProviderData>, + providerDataList: List<GetCredentialProviderData>, context: Context, ): List<ProviderInfo> { return providerDataList.map { @@ -36,9 +37,10 @@ class GetFlowUtils { // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, name = it.providerFlattenedComponentName, - displayName = it.providerDisplayName, + // TODO: get the service display name and icon from the component name. + displayName = it.providerFlattenedComponentName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, - credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context) + credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context), ) } } @@ -72,7 +74,7 @@ class CreateFlowUtils { companion object { fun toProviderList( - providerDataList: List<ProviderData>, + providerDataList: List<CreateCredentialProviderData>, context: Context, ): List<com.android.credentialmanager.createflow.ProviderInfo> { return providerDataList.map { @@ -80,9 +82,11 @@ class CreateFlowUtils { // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, name = it.providerFlattenedComponentName, - displayName = it.providerDisplayName, + // TODO: get the service display name and icon from the component name. + displayName = it.providerFlattenedComponentName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, - createOptions = toCreationOptionInfoList(it.credentialEntries, context), + createOptions = toCreationOptionInfoList(it.saveEntries, context), + isDefault = it.isDefaultProvider, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index cb2bf10f2aef..db0f337e9a2b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -24,6 +24,7 @@ data class ProviderInfo( val displayName: String, val credentialTypeIcon: Drawable, val createOptions: List<CreateOptionInfo>, + val isDefault: Boolean, ) data class CreateOptionInfo( diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index e30d4415a0c4..8d4431520c75 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -35,4 +35,6 @@ <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at fade_out_complete_frame --> <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> + + <integer name="qs_carrier_max_em">7</integer> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 93ee151f26c5..c756a17976bf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -89,6 +89,7 @@ import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; +import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; @@ -136,6 +137,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { private GlobalSettings mGlobalSettings; private FalsingManager mFalsingManager; private UserSwitcherController mUserSwitcherController; + private FalsingA11yDelegate mFalsingA11yDelegate; private AlertDialog mAlertDialog; private boolean mSwipeUpToRetry; @@ -318,7 +320,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager, UserSwitcherController userSwitcherController, - UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback) { + UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback, + FalsingA11yDelegate falsingA11yDelegate) { if (mCurrentMode == mode) return; Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to " + modeToString(mode)); @@ -337,6 +340,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } mGlobalSettings = globalSettings; mFalsingManager = falsingManager; + mFalsingA11yDelegate = falsingA11yDelegate; mUserSwitcherController = userSwitcherController; setupViewMode(); } @@ -361,7 +365,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } mViewMode.init(this, mGlobalSettings, mSecurityViewFlipper, mFalsingManager, - mUserSwitcherController); + mUserSwitcherController, mFalsingA11yDelegate); } @Mode int getMode() { @@ -723,7 +727,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { default void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, - @NonNull UserSwitcherController userSwitcherController) {}; + @NonNull UserSwitcherController userSwitcherController, + @NonNull FalsingA11yDelegate falsingA11yDelegate) {}; /** Reinitialize the location */ default void updateSecurityViewLocation() {}; @@ -828,7 +833,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, - @NonNull UserSwitcherController userSwitcherController) { + @NonNull UserSwitcherController userSwitcherController, + @NonNull FalsingA11yDelegate falsingA11yDelegate) { mView = v; mViewFlipper = viewFlipper; @@ -865,6 +871,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { this::setupUserSwitcher; private UserSwitcherCallback mUserSwitcherCallback; + private FalsingA11yDelegate mFalsingA11yDelegate; UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) { mUserSwitcherCallback = userSwitcherCallback; @@ -874,13 +881,15 @@ public class KeyguardSecurityContainer extends ConstraintLayout { public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, - @NonNull UserSwitcherController userSwitcherController) { + @NonNull UserSwitcherController userSwitcherController, + @NonNull FalsingA11yDelegate falsingA11yDelegate) { init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false); mView = v; mViewFlipper = viewFlipper; mFalsingManager = falsingManager; mUserSwitcherController = userSwitcherController; mResources = v.getContext().getResources(); + mFalsingA11yDelegate = falsingA11yDelegate; if (mUserSwitcherViewGroup == null) { LayoutInflater.from(v.getContext()).inflate( @@ -978,6 +987,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { mUserSwitcher.setText(currentUserName); KeyguardUserSwitcherAnchor anchor = mView.findViewById(R.id.user_switcher_anchor); + anchor.setAccessibilityDelegate(mFalsingA11yDelegate); BaseUserSwitcherAdapter adapter = new BaseUserSwitcherAdapter(mUserSwitcherController) { @Override @@ -1048,7 +1058,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { anchor.setOnClickListener((v) -> { if (mFalsingManager.isFalseTap(LOW_PENALTY)) return; - mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager); + mPopup = new KeyguardUserSwitcherPopupMenu(mView.getContext(), mFalsingManager); mPopup.setAnchorView(anchor); mPopup.setAdapter(adapter); mPopup.setOnItemClickListener((parent, view, pos, id) -> { @@ -1137,7 +1147,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, - @NonNull UserSwitcherController userSwitcherController) { + @NonNull UserSwitcherController userSwitcherController, + @NonNull FalsingA11yDelegate falsingA11yDelegate) { init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true); mView = v; mViewFlipper = viewFlipper; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 0b395a8760cf..79a01b9c9717 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -59,6 +59,7 @@ import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.biometrics.SidefpsController; +import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -100,6 +101,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final FeatureFlags mFeatureFlags; private final SessionTracker mSessionTracker; private final Optional<SidefpsController> mSidefpsController; + private final FalsingA11yDelegate mFalsingA11yDelegate; private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; @@ -288,7 +290,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard FeatureFlags featureFlags, GlobalSettings globalSettings, SessionTracker sessionTracker, - Optional<SidefpsController> sidefpsController) { + Optional<SidefpsController> sidefpsController, + FalsingA11yDelegate falsingA11yDelegate) { super(view); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; @@ -309,6 +312,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mGlobalSettings = globalSettings; mSessionTracker = sessionTracker; mSidefpsController = sidefpsController; + mFalsingA11yDelegate = falsingA11yDelegate; } @Override @@ -349,10 +353,21 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (!mSidefpsController.isPresent()) { return; } - if (mBouncerVisible - && getResources().getBoolean(R.bool.config_show_sidefps_hint_on_bouncer) - && mUpdateMonitor.isFingerprintDetectionRunning() - && !mUpdateMonitor.userNeedsStrongAuth()) { + final boolean sfpsEnabled = getResources().getBoolean( + R.bool.config_show_sidefps_hint_on_bouncer); + final boolean fpsDetectionRunning = mUpdateMonitor.isFingerprintDetectionRunning(); + final boolean needsStrongAuth = mUpdateMonitor.userNeedsStrongAuth(); + + boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning && !needsStrongAuth; + + if (DEBUG) { + Log.d(TAG, "sideFpsToShow=" + toShow + ", " + + "mBouncerVisible=" + mBouncerVisible + ", " + + "configEnabled=" + sfpsEnabled + ", " + + "fpsDetectionRunning=" + fpsDetectionRunning + ", " + + "needsStrongAuth=" + needsStrongAuth); + } + if (toShow) { mSidefpsController.get().show(); } else { mSidefpsController.get().hide(); @@ -625,7 +640,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue), - null)); + null), mFalsingA11yDelegate); } public void reportFailedUnlockAttempt(int userId, int timeoutMs) { @@ -730,6 +745,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final UserSwitcherController mUserSwitcherController; private final SessionTracker mSessionTracker; private final Optional<SidefpsController> mSidefpsController; + private final FalsingA11yDelegate mFalsingA11yDelegate; @Inject Factory(KeyguardSecurityContainer view, @@ -749,7 +765,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard FeatureFlags featureFlags, GlobalSettings globalSettings, SessionTracker sessionTracker, - Optional<SidefpsController> sidefpsController) { + Optional<SidefpsController> sidefpsController, + FalsingA11yDelegate falsingA11yDelegate) { mView = view; mAdminSecondaryLockScreenControllerFactory = adminSecondaryLockScreenControllerFactory; mLockPatternUtils = lockPatternUtils; @@ -767,6 +784,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mUserSwitcherController = userSwitcherController; mSessionTracker = sessionTracker; mSidefpsController = sidefpsController; + mFalsingA11yDelegate = falsingA11yDelegate; } public KeyguardSecurityContainerController create( @@ -777,7 +795,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mKeyguardStateController, securityCallback, mSecurityViewFlipperController, mConfigurationController, mFalsingCollector, mFalsingManager, mUserSwitcherController, mFeatureFlags, mGlobalSettings, mSessionTracker, - mSidefpsController); + mSidefpsController, mFalsingA11yDelegate); } } } diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt index 22dc94a2c3f9..5850c9537ef0 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt @@ -21,6 +21,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.os.Handler import android.os.Looper +import android.os.Trace import android.os.UserHandle import android.util.ArrayMap import android.util.ArraySet @@ -126,6 +127,7 @@ open class UserBroadcastDispatcher( action, userId, { + Trace.beginSection("registerReceiver act=$action user=$userId") context.registerReceiverAsUser( this, UserHandle.of(userId), @@ -134,11 +136,14 @@ open class UserBroadcastDispatcher( workerHandler, flags ) + Trace.endSection() logger.logContextReceiverRegistered(userId, flags, it) }, { try { + Trace.beginSection("unregisterReceiver act=$action user=$userId") context.unregisterReceiver(this) + Trace.endSection() logger.logContextReceiverUnregistered(userId, action) } catch (e: IllegalArgumentException) { Log.e(TAG, "Trying to unregister unregistered receiver for user $userId, " + diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index 500f28004429..2245d8462c31 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -337,7 +337,8 @@ public class BrightLineFalsingManager implements FalsingManager { || mTestHarness || mDataProvider.isJustUnlockedWithFace() || mDataProvider.isDocked() - || mAccessibilityManager.isTouchExplorationEnabled(); + || mAccessibilityManager.isTouchExplorationEnabled() + || mDataProvider.isA11yAction(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt new file mode 100644 index 000000000000..63d57cc3fc8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt @@ -0,0 +1,38 @@ +/* + * 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.systemui.classifier + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK +import javax.inject.Inject + +/** + * Class that injects an artificial tap into the falsing collector. + * + * This is used for views that can be interacted with by A11y services and have falsing checks, as + * the gestures made by the A11y framework do not propagate motion events down the view hierarchy. + */ +class FalsingA11yDelegate @Inject constructor(private val falsingCollector: FalsingCollector) : + View.AccessibilityDelegate() { + override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + if (action == ACTION_CLICK) { + falsingCollector.onA11yAction() + } + return super.performAccessibilityAction(host, action, args) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index 858bac30880b..66701080ddfb 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -132,5 +132,8 @@ public interface FalsingCollector { /** */ void updateFalseConfidence(FalsingClassifier.Result result); + + /** Indicates an a11y action was made. */ + void onA11yAction(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index 0b7d6ab5acf7..cc25368161eb 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -157,4 +157,8 @@ public class FalsingCollectorFake implements FalsingCollector { @Override public void updateFalseConfidence(FalsingClassifier.Result result) { } + + @Override + public void onA11yAction() { + } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index da3d293d543b..8bdef1304fa4 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -375,6 +375,15 @@ class FalsingCollectorImpl implements FalsingCollector { mHistoryTracker.addResults(Collections.singleton(result), mSystemClock.uptimeMillis()); } + @Override + public void onA11yAction() { + if (mPendingDownEvent != null) { + mPendingDownEvent.recycle(); + mPendingDownEvent = null; + } + mFalsingDataProvider.onA11yAction(); + } + private boolean shouldSessionBeActive() { return mScreenOn && (mState == StatusBarState.KEYGUARD) && !mShowingAod; } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 3991a35e958a..09ebeeac163f 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -59,6 +59,7 @@ public class FalsingDataProvider { private MotionEvent mFirstRecentMotionEvent; private MotionEvent mLastMotionEvent; private boolean mJustUnlockedWithFace; + private boolean mA11YAction; @Inject public FalsingDataProvider( @@ -124,6 +125,7 @@ public class FalsingDataProvider { mPriorMotionEvents = mRecentMotionEvents; mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } + mA11YAction = false; } /** Returns screen width in pixels. */ @@ -334,6 +336,17 @@ public class FalsingDataProvider { mGestureFinalizedListeners.remove(listener); } + /** Return whether last gesture was an A11y action. */ + public boolean isA11yAction() { + return mA11YAction; + } + + /** Set whether last gesture was an A11y action. */ + public void onA11yAction() { + completePriorGesture(); + this.mA11YAction = true; + } + void onSessionStarted() { mSessionListeners.forEach(SessionListener::onSessionStarted); } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index bf7d71635694..6cb0e8b04869 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -24,7 +24,6 @@ import android.app.ActivityOptions import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.service.controls.Control @@ -59,7 +58,10 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy @@ -76,13 +78,14 @@ class ControlsUiControllerImpl @Inject constructor ( @Main val uiExecutor: DelayableExecutor, @Background val bgExecutor: DelayableExecutor, val controlsListingController: Lazy<ControlsListingController>, - @Main val sharedPreferences: SharedPreferences, val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, private val shadeController: ShadeController, private val iconCache: CustomIconCache, private val controlsMetricsLogger: ControlsMetricsLogger, - private val keyguardStateController: KeyguardStateController + private val keyguardStateController: KeyguardStateController, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, ) : ControlsUiController { companion object { @@ -110,6 +113,12 @@ class ControlsUiControllerImpl @Inject constructor ( private lateinit var onDismiss: Runnable private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private var retainCache = false + private val sharedPreferences + get() = userFileManager.getSharedPreferences( + fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + mode = 0, + userId = userTracker.userId + ) private val collator = Collator.getInstance(context.resources.configuration.locales[0]) private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 48bef97c30fb..2bee75e9435b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -41,6 +41,7 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.screenshot.ReferenceScreenshotModule; +import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeControllerImpl; @@ -93,6 +94,7 @@ import dagger.Provides; AospPolicyModule.class, GestureModule.class, MediaModule.class, + MultiUserUtilsModule.class, PowerModule.class, QSModule.class, ReferenceScreenshotModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 6db562107357..482bdafc63cc 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -58,7 +58,6 @@ import com.android.systemui.qs.footer.dagger.FooterActionsModule; import com.android.systemui.recents.Recents; import com.android.systemui.screenshot.dagger.ScreenshotModule; import com.android.systemui.security.data.repository.SecurityRepositoryModule; -import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.ShadeController; import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.statusbar.CommandQueue; @@ -140,7 +139,6 @@ import dagger.Provides; PrivacyModule.class, ScreenshotModule.class, SensorModule.class, - MultiUserUtilsModule.class, SecurityRepositoryModule.class, SettingsUtilModule.class, SmartRepliesInflationModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 7b1ba0d17b6f..b5d4ecd5d753 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -308,7 +308,7 @@ object Flags { // 1300 - screenshots // TODO(b/254512719): Tracking Bug - @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300) + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300, true) // TODO(b/254513155): Tracking Bug @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301) diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java index 703b95a082dc..b5ceeaed4904 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java @@ -19,6 +19,7 @@ package com.android.systemui.qs.carrier; import android.annotation.StyleRes; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -33,6 +34,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.util.LargeScreenUtils; import java.util.Objects; @@ -72,6 +74,7 @@ public class QSCarrier extends LinearLayout { mMobileSignal = findViewById(R.id.mobile_signal); mCarrierText = findViewById(R.id.qs_carrier_text); mSpacer = findViewById(R.id.spacer); + updateResources(); } /** @@ -142,4 +145,20 @@ public class QSCarrier extends LinearLayout { public void updateTextAppearance(@StyleRes int resId) { FontSizeUtils.updateFontSizeFromStyle(mCarrierText, resId); } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateResources(); + } + + private void updateResources() { + boolean useLargeScreenHeader = + LargeScreenUtils.shouldUseLargeScreenShadeHeader(getResources()); + mCarrierText.setMaxEms( + useLargeScreenHeader + ? Integer.MAX_VALUE + : getResources().getInteger(R.integer.qs_carrier_max_em) + ); + } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 671173413e73..cd5647e51029 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -30,7 +30,6 @@ import androidx.annotation.GuardedBy import androidx.annotation.WorkerThread import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager -import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE import com.android.systemui.util.Assert import java.io.PrintWriter import java.lang.ref.WeakReference @@ -53,7 +52,7 @@ import kotlin.reflect.KProperty * * Class constructed and initialized in [SettingsModule]. */ -class UserTrackerImpl internal constructor( +open class UserTrackerImpl internal constructor( private val context: Context, private val userManager: UserManager, private val dumpManager: DumpManager, @@ -70,13 +69,13 @@ class UserTrackerImpl internal constructor( private val mutex = Any() override var userId: Int by SynchronizedDelegate(context.userId) - private set + protected set override var userHandle: UserHandle by SynchronizedDelegate(context.user) - private set + protected set override var userContext: Context by SynchronizedDelegate(context) - private set + protected set override val userContentResolver: ContentResolver get() = userContext.contentResolver @@ -94,7 +93,7 @@ class UserTrackerImpl internal constructor( * modified. */ override var userProfiles: List<UserInfo> by SynchronizedDelegate(emptyList()) - private set + protected set @GuardedBy("callbacks") private val callbacks: MutableList<DataItem> = ArrayList() @@ -155,7 +154,7 @@ class UserTrackerImpl internal constructor( } @WorkerThread - private fun handleSwitchUser(newUser: Int) { + protected open fun handleSwitchUser(newUser: Int) { Assert.isNotMainThread() if (newUser == UserHandle.USER_NULL) { Log.w(TAG, "handleSwitchUser - Couldn't get new id from intent") @@ -174,7 +173,7 @@ class UserTrackerImpl internal constructor( } @WorkerThread - private fun handleProfilesChanged() { + protected open fun handleProfilesChanged() { Assert.isNotMainThread() val profiles = userManager.getProfiles(userId) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 760d22efc32c..24501972e375 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -33,7 +33,6 @@ import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; import static com.android.systemui.classifier.Classifier.UNLOCK; -import static com.android.systemui.shade.NotificationPanelView.DEBUG; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING; @@ -42,7 +41,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_Q import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES; -import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; import static com.android.systemui.util.DumpUtilsKt.asIndenting; @@ -52,10 +50,10 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Fragment; import android.app.StatusBarManager; import android.content.ContentResolver; -import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Canvas; @@ -103,7 +101,6 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; -import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; @@ -174,16 +171,13 @@ import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.ViewGroupFadeHelper; -import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -209,7 +203,6 @@ import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController; import com.android.systemui.statusbar.phone.LockscreenGestureLogger; import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.PhoneStatusBarView; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; @@ -256,28 +249,15 @@ public final class NotificationPanelViewController { private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private static final boolean DEBUG_DRAWABLE = false; - private static final VibrationEffect ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false); - - /** - * The parallax amount of the quick settings translation when dragging down the panel - */ + /** The parallax amount of the quick settings translation when dragging down the panel. */ private static final float QS_PARALLAX_AMOUNT = 0.175f; - - /** - * Fling expanding QS. - */ + /** Fling expanding QS. */ public static final int FLING_EXPAND = 0; - - /** - * Fling collapsing QS, potentially stopping when QS becomes QQS. - */ + /** Fling collapsing QS, potentially stopping when QS becomes QQS. */ private static final int FLING_COLLAPSE = 1; - - /** - * Fling until QS is completely hidden. - */ + /** Fling until QS is completely hidden. */ private static final int FLING_HIDE = 2; private static final long ANIMATION_DELAY_ICON_FADE_IN = ActivityLaunchAnimator.TIMINGS.getTotalDuration() @@ -291,6 +271,18 @@ public final class NotificationPanelViewController { * when flinging. A low value will make it that most flings will reach the maximum overshoot. */ private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f; + /** + * Maximum time before which we will expand the panel even for slow motions when getting a + * touch passed over from launcher. + */ + private static final int MAX_TIME_TO_OPEN_WHEN_FLINGING_FROM_LAUNCHER = 300; + private static final int MAX_DOWN_EVENT_BUFFER_SIZE = 50; + private static final String COUNTER_PANEL_OPEN = "panel_open"; + private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs"; + private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek"; + private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1); + private static final Rect EMPTY_RECT = new Rect(); + private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; private final Resources mResources; private final KeyguardStateController mKeyguardStateController; @@ -299,49 +291,24 @@ public final class NotificationPanelViewController { private final LockscreenGestureLogger mLockscreenGestureLogger; private final SystemClock mSystemClock; private final ShadeLogger mShadeLog; - private final DozeParameters mDozeParameters; - private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener(); - private final Runnable mCollapseExpandAction = new CollapseExpandAction(); - private final OnOverscrollTopChangedListener - mOnOverscrollTopChangedListener = - new OnOverscrollTopChangedListener(); - private final OnEmptySpaceClickListener - mOnEmptySpaceClickListener = - new OnEmptySpaceClickListener(); - private final MyOnHeadsUpChangedListener - mOnHeadsUpChangedListener = - new MyOnHeadsUpChangedListener(); - private final HeightListener mHeightListener = new HeightListener(); + private final Runnable mCollapseExpandAction = this::collapseOrExpand; + private final NsslOverscrollTopChangedListener mOnOverscrollTopChangedListener = + new NsslOverscrollTopChangedListener(); + private final NotificationStackScrollLayout.OnEmptySpaceClickListener + mOnEmptySpaceClickListener = (x, y) -> onEmptySpaceClick(); + private final ShadeHeadsUpChangedListener mOnHeadsUpChangedListener = + new ShadeHeadsUpChangedListener(); + private final QS.HeightListener mHeightListener = this::onQsHeightChanged; private final ConfigurationListener mConfigurationListener = new ConfigurationListener(); private final SettingsChangeObserver mSettingsChangeObserver; - - @VisibleForTesting - final StatusBarStateListener mStatusBarStateListener = - new StatusBarStateListener(); + private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener(); private final NotificationPanelView mView; private final VibratorHelper mVibratorHelper; private final MetricsLogger mMetricsLogger; private final ConfigurationController mConfigurationController; private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder; private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; - private final NotificationIconAreaController mNotificationIconAreaController; - - /** - * Maximum time before which we will expand the panel even for slow motions when getting a - * touch passed over from launcher. - */ - private static final int MAX_TIME_TO_OPEN_WHEN_FLINGING_FROM_LAUNCHER = 300; - - private static final int MAX_DOWN_EVENT_BUFFER_SIZE = 50; - - private static final String COUNTER_PANEL_OPEN = "panel_open"; - private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs"; - private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek"; - - private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1); - private static final Rect EMPTY_RECT = new Rect(); - private final InteractionJankMonitor mInteractionJankMonitor; private final LayoutInflater mLayoutInflater; private final FeatureFlags mFeatureFlags; @@ -361,9 +328,7 @@ public final class NotificationPanelViewController { private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory; private final FragmentService mFragmentService; private final ScrimController mScrimController; - private final PrivacyDotViewController mPrivacyDotViewController; private final NotificationRemoteInputManager mRemoteInputManager; - private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; private final ShadeTransitionController mShadeTransitionController; private final TapAgainViewController mTapAgainViewController; @@ -380,6 +345,11 @@ public final class NotificationPanelViewController { private final Interpolator mBounceInterpolator; private final NotificationShadeWindowController mNotificationShadeWindowController; private final ShadeExpansionStateManager mShadeExpansionStateManager; + private final QS.ScrollListener mQsScrollListener = this::onQsPanelScrollChanged; + private final FalsingTapListener mFalsingTapListener = this::falsingAdditionalTapRequired; + private final FragmentListener mQsFragmentListener = new QsFragmentListener(); + private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); + private long mDownTime; private boolean mTouchSlopExceededBeforeDown; private boolean mIsLaunchAnimationRunning; @@ -401,13 +371,11 @@ public final class NotificationPanelViewController { private float mKeyguardNotificationTopPadding; /** Current max allowed keyguard notifications determined by measuring the panel. */ private int mMaxAllowedKeyguardNotifications; - private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController; private KeyguardUserSwitcherController mKeyguardUserSwitcherController; private KeyguardStatusBarView mKeyguardStatusBar; private KeyguardStatusBarViewController mKeyguardStatusBarViewController; - @VisibleForTesting - QS mQs; + private QS mQs; private FrameLayout mQsFrame; private final QsFrameTranslateController mQsFrameTranslateController; private KeyguardStatusViewController mKeyguardStatusViewController; @@ -420,18 +388,11 @@ public final class NotificationPanelViewController { private float mQuickQsHeaderHeight; private final ScreenOffAnimationController mScreenOffAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; - private int mQsTrackingPointer; private VelocityTracker mQsVelocityTracker; private boolean mQsTracking; - - /** - * If set, the ongoing touch gesture might both trigger the expansion in {@link - * NotificationPanelView} and - * the expansion for quick settings. - */ + /** Whether the ongoing gesture might both trigger the expansion in both the view and QS. */ private boolean mConflictingQsExpansionGesture; - private boolean mPanelExpanded; /** @@ -486,11 +447,9 @@ public final class NotificationPanelViewController { * Used for split shade, two finger gesture as well as accessibility shortcut to QS. * It needs to be set when movement starts as it resets at the end of expansion/collapse. */ - @VisibleForTesting - boolean mQsExpandImmediate; + private boolean mQsExpandImmediate; private boolean mTwoFingerQsExpandPossible; private String mHeaderDebugInfo; - /** * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still * need to take this into account in our panel height calculation. @@ -498,7 +457,6 @@ public final class NotificationPanelViewController { private boolean mQsAnimatorExpand; private boolean mIsLaunchTransitionFinished; private ValueAnimator mQsSizeChangeAnimator; - private boolean mQsScrimEnabled = true; private boolean mQsTouchAboveFalsingThreshold; private int mQsFalsingThreshold; @@ -516,39 +474,27 @@ public final class NotificationPanelViewController { private final FalsingManager mFalsingManager; private final FalsingCollector mFalsingCollector; - private final Runnable mHeadsUpExistenceChangedRunnable = () -> { - setHeadsUpAnimatingAway(false); - updatePanelExpansionAndVisibility(); - }; private boolean mShowIconsWhenExpanded; private int mIndicationBottomPadding; private int mAmbientIndicationBottomPadding; + /** Whether the notifications are displayed full width (no margins on the side). */ private boolean mIsFullWidth; private boolean mBlockingExpansionForCurrentTouch; + // Following variables maintain state of events when input focus transfer may occur. + private boolean mExpectingSynthesizedDown; + private boolean mLastEventSynthesizedDown; - /** - * Following variables maintain state of events when input focus transfer may occur. - */ - private boolean mExpectingSynthesizedDown; // expecting to see synthesized DOWN event - private boolean mLastEventSynthesizedDown; // last event was synthesized DOWN event - - /** - * Current dark amount that follows regular interpolation curve of animation. - */ + /** Current dark amount that follows regular interpolation curve of animation. */ private float mInterpolatedDarkAmount; - /** * Dark amount that animates from 0 to 1 or vice-versa in linear manner, even if the * interpolation curve is different. */ private float mLinearDarkAmount; - private boolean mPulsing; private boolean mHideIconsDuringLaunchAnimation = true; private int mStackScrollerMeasuringPass; - /** - * Non-null if there's a heads-up notification that we're currently tracking the position of. - */ + /** Non-null if a heads-up notification's position is being tracked. */ @Nullable private ExpandableNotificationRow mTrackedHeadsUpNotification; private final ArrayList<Consumer<ExpandableNotificationRow>> @@ -578,8 +524,9 @@ public final class NotificationPanelViewController { private final CommandQueue mCommandQueue; private final UserManager mUserManager; private final MediaDataManager mMediaDataManager; + @PanelState + private int mCurrentPanelState = STATE_CLOSED; private final SysUiState mSysUiState; - private final NotificationShadeDepthController mDepthController; private final NavigationBarController mNavigationBarController; private final int mDisplayId; @@ -589,6 +536,7 @@ public final class NotificationPanelViewController { private boolean mHeadsUpPinnedMode; private boolean mAllowExpandForSmallExpansion; private Runnable mExpandAfterLayoutRunnable; + private Runnable mHideExpandedRunnable; /** * The padding between the start of notifications and the qs boundary on the lockscreen. @@ -596,94 +544,51 @@ public final class NotificationPanelViewController { * qs boundary to be padded. */ private int mLockscreenNotificationQSPadding; - /** * The amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full * shade. This value can also go beyond 1.1 when we're overshooting! */ private float mTransitioningToFullShadeProgress; - /** * Position of the qs bottom during the full shade transition. This is needed as the toppadding * can change during state changes, which makes it much harder to do animations */ private int mTransitionToFullShadeQSPosition; - - /** - * Distance that the full shade transition takes in order for qs to fully transition to the - * shade. - */ + /** Distance a full shade transition takes in order for qs to fully transition to the shade. */ private int mDistanceForQSFullShadeTransition; - - /** - * The translation amount for QS for the full shade transition - */ + /** The translation amount for QS for the full shade transition. */ private float mQsTranslationForFullShadeTransition; - /** - * The maximum overshoot allowed for the top padding for the full shade transition - */ + /** The maximum overshoot allowed for the top padding for the full shade transition. */ private int mMaxOverscrollAmountForPulse; - - /** - * Should we animate the next bounds update - */ + /** Should we animate the next bounds update. */ private boolean mAnimateNextNotificationBounds; - /** - * The delay for the next bounds animation - */ + /** The delay for the next bounds animation. */ private long mNotificationBoundsAnimationDelay; - - /** - * The duration of the notification bounds animation - */ + /** The duration of the notification bounds animation. */ private long mNotificationBoundsAnimationDuration; - /** - * Is this a collapse that started on the panel where we should allow the panel to intercept - */ + /** Whether a collapse that started on the panel should allow the panel to intercept. */ private boolean mIsPanelCollapseOnQQS; - private boolean mAnimatingQS; - - /** - * The end bounds of a clipping animation. - */ + /** The end bounds of a clipping animation. */ private final Rect mQsClippingAnimationEndBounds = new Rect(); - - /** - * The animator for the qs clipping bounds. - */ + /** The animator for the qs clipping bounds. */ private ValueAnimator mQsClippingAnimation = null; - - /** - * Is the current animator resetting the qs translation. - */ + /** Whether the current animator is resetting the qs translation. */ private boolean mIsQsTranslationResetAnimator; - /** - * Is the current animator resetting the pulse expansion after a drag down - */ + /** Whether the current animator is resetting the pulse expansion after a drag down. */ private boolean mIsPulseExpansionResetAnimator; private final Rect mKeyguardStatusAreaClipBounds = new Rect(); private final Region mQsInterceptRegion = new Region(); - - /** - * The alpha of the views which only show on the keyguard but not in shade / shade locked - */ + /** Alpha of the views which only show on the keyguard but not in shade / shade locked. */ private float mKeyguardOnlyContentAlpha = 1.0f; - - /** - * The translationY of the views which only show on the keyguard but in shade / shade locked. - */ + /** Y translation of the views that only show on the keyguard but in shade / shade locked. */ private int mKeyguardOnlyTransitionTranslationY = 0; - private float mUdfpsMaxYBurnInOffset; - - /** - * Are we currently in gesture navigation - */ + /** Are we currently in gesture navigation. */ private boolean mIsGestureNavigation; private int mOldLayoutDirection; private NotificationShelfController mNotificationShelfController; @@ -696,6 +601,7 @@ public final class NotificationPanelViewController { private int mQsClipTop; private int mQsClipBottom; private boolean mQsVisible; + private final ContentResolver mContentResolver; private float mMinFraction; @@ -714,55 +620,7 @@ public final class NotificationPanelViewController { private final NotificationListContainer mNotificationListContainer; private final NotificationStackSizeCalculator mNotificationStackSizeCalculator; - private final NPVCDownEventState.Buffer mLastDownEvents; - - private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = - () -> mKeyguardBottomArea.setVisibility(View.GONE); - - private final AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (action - == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId() - || action - == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) { - mStatusBarKeyguardViewManager.showBouncer(true); - return true; - } - return super.performAccessibilityAction(host, action, args); - } - }; - - private final FalsingTapListener mFalsingTapListener = new FalsingTapListener() { - @Override - public void onAdditionalTapRequired() { - if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { - mTapAgainViewController.show(); - } else { - mKeyguardIndicationController.showTransientIndication( - R.string.notification_tap_again); - } - - if (!mStatusBarStateController.isDozing()) { - mVibratorHelper.vibrate( - Process.myUid(), - mView.getContext().getPackageName(), - ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT, - "falsing-additional-tap-required", - TOUCH_VIBRATION_ATTRIBUTES); - } - } - }; - private final CameraGestureHelper mCameraGestureHelper; private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @@ -812,8 +670,20 @@ public final class NotificationPanelViewController { private boolean mGestureWaitForTouchSlop; private boolean mIgnoreXTouchSlop; private boolean mExpandLatencyTracking; + private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); + private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = + () -> mKeyguardBottomArea.setVisibility(View.GONE); + private final Runnable mHeadsUpExistenceChangedRunnable = () -> { + setHeadsUpAnimatingAway(false); + updatePanelExpansionAndVisibility(); + }; + private final Runnable mMaybeHideExpandedRunnable = () -> { + if (getExpansionFraction() == 0.0f) { + getView().post(mHideExpandedRunnable); + } + }; @Inject public NotificationPanelViewController(NotificationPanelView view, @@ -848,7 +718,6 @@ public final class NotificationPanelViewController { KeyguardUserSwitcherComponent.Factory keyguardUserSwitcherComponentFactory, KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory, LockscreenShadeTransitionController lockscreenShadeTransitionController, - NotificationIconAreaController notificationIconAreaController, AuthController authController, ScrimController scrimController, UserManager userManager, @@ -857,7 +726,6 @@ public final class NotificationPanelViewController { AmbientState ambientState, LockIconViewController lockIconViewController, KeyguardMediaController keyguardMediaController, - PrivacyDotViewController privacyDotViewController, TapAgainViewController tapAgainViewController, NavigationModeController navigationModeController, NavigationBarController navigationBarController, @@ -895,7 +763,6 @@ public final class NotificationPanelViewController { mLockscreenGestureLogger = lockscreenGestureLogger; mShadeExpansionStateManager = shadeExpansionStateManager; mShadeLog = shadeLogger; - TouchHandler touchHandler = createTouchHandler(); mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -903,13 +770,12 @@ public final class NotificationPanelViewController { } @Override - public void onViewDetachedFromWindow(View v) { - } + public void onViewDetachedFromWindow(View v) {} }); - mView.addOnLayoutChangeListener(createLayoutChangeListener()); - mView.setOnTouchListener(touchHandler); - mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener()); + mView.addOnLayoutChangeListener(new ShadeLayoutChangeListener()); + mView.setOnTouchListener(createTouchHandler()); + mView.setOnConfigurationChangedListener(config -> loadDimens()); mResources = mView.getResources(); mKeyguardStateController = keyguardStateController; @@ -945,7 +811,6 @@ public final class NotificationPanelViewController { mInteractionJankMonitor = interactionJankMonitor; mSystemClock = systemClock; mKeyguardMediaController = keyguardMediaController; - mPrivacyDotViewController = privacyDotViewController; mMetricsLogger = metricsLogger; mConfigurationController = configurationController; mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; @@ -957,7 +822,6 @@ public final class NotificationPanelViewController { mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider; mNotificationsQSContainerController.init(); mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; - mNotificationIconAreaController = notificationIconAreaController; mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory; mDepthController = notificationShadeDepthController; @@ -1000,10 +864,7 @@ public final class NotificationPanelViewController { mShadeTransitionController = shadeTransitionController; lockscreenShadeTransitionController.setNotificationPanelController(this); shadeTransitionController.setNotificationPanelViewController(this); - DynamicPrivacyControlListener - dynamicPrivacyControlListener = - new DynamicPrivacyControlListener(); - dynamicPrivacyController.addListener(dynamicPrivacyControlListener); + dynamicPrivacyController.addListener(this::onDynamicPrivacyChanged); shadeExpansionStateManager.addStateListener(this::onPanelStateChanged); @@ -1027,13 +888,14 @@ public final class NotificationPanelViewController { mIsGestureNavigation = QuickStepContract.isGesturalMode(currentMode); mView.setBackgroundColor(Color.TRANSPARENT); - OnAttachStateChangeListener onAttachStateChangeListener = new OnAttachStateChangeListener(); + ShadeAttachStateChangeListener + onAttachStateChangeListener = new ShadeAttachStateChangeListener(); mView.addOnAttachStateChangeListener(onAttachStateChangeListener); if (mView.isAttachedToWindow()) { onAttachStateChangeListener.onViewAttachedToWindow(mView); } - mView.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener()); + mView.setOnApplyWindowInsetsListener((v, insets) -> onApplyShadeWindowInsets(insets)); if (DEBUG_DRAWABLE) { mView.getOverlay().add(new DebugDrawable()); @@ -1052,57 +914,68 @@ public final class NotificationPanelViewController { new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() { @Override public void onUnlockAnimationFinished() { - // Make sure the clock is in the correct position after the unlock animation - // so that it's not in the wrong place when we show the keyguard again. - positionClockAndNotifications(true /* forceClockUpdate */); + unlockAnimationFinished(); } @Override public void onUnlockAnimationStarted( boolean playingCannedAnimation, boolean isWakeAndUnlock, - long unlockAnimationStartDelay, + long startDelay, long unlockAnimationDuration) { - // Disable blurs while we're unlocking so that panel expansion does not - // cause blurring. This will eventually be re-enabled by the panel view on - // ACTION_UP, since the user's finger might still be down after a swipe to - // unlock gesture, and we don't want that to cause blurring either. - mDepthController.setBlursDisabledForUnlock(mTracking); - - if (playingCannedAnimation && !isWakeAndUnlock) { - // Hide the panel so it's not in the way or the surface behind the - // keyguard, which will be appearing. If we're wake and unlocking, the - // lock screen is hidden instantly so should not be flung away. - if (isTracking() || isFlinging()) { - // Instant collpase the notification panel since the notification - // panel is already in the middle animating - onTrackingStopped(false); - instantCollapse(); - } else { - mView.animate() - .alpha(0f) - .setStartDelay(0) - // Translate up by 4%. - .translationY(mView.getHeight() * -0.04f) - // This start delay is to give us time to animate out before - // the launcher icons animation starts, so use that as our - // duration. - .setDuration(unlockAnimationStartDelay) - .setInterpolator(EMPHASIZED_ACCELERATE) - .withEndAction(() -> { - instantCollapse(); - mView.setAlpha(1f); - mView.setTranslationY(0f); - }) - .start(); - } - } + unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay); } }); mCameraGestureHelper = cameraGestureHelper; mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor; } + private void unlockAnimationFinished() { + // Make sure the clock is in the correct position after the unlock animation + // so that it's not in the wrong place when we show the keyguard again. + positionClockAndNotifications(true /* forceClockUpdate */); + } + + private void unlockAnimationStarted( + boolean playingCannedAnimation, + boolean isWakeAndUnlock, + long unlockAnimationStartDelay) { + // Disable blurs while we're unlocking so that panel expansion does not + // cause blurring. This will eventually be re-enabled by the panel view on + // ACTION_UP, since the user's finger might still be down after a swipe to + // unlock gesture, and we don't want that to cause blurring either. + mDepthController.setBlursDisabledForUnlock(mTracking); + + if (playingCannedAnimation && !isWakeAndUnlock) { + // Hide the panel so it's not in the way or the surface behind the + // keyguard, which will be appearing. If we're wake and unlocking, the + // lock screen is hidden instantly so should not be flung away. + if (isTracking() || mIsFlinging) { + // Instant collapse the notification panel since the notification + // panel is already in the middle animating + onTrackingStopped(false); + instantCollapse(); + } else { + mView.animate() + .alpha(0f) + .setStartDelay(0) + // Translate up by 4%. + .translationY(mView.getHeight() * -0.04f) + // This start delay is to give us time to animate out before + // the launcher icons animation starts, so use that as our + // duration. + .setDuration(unlockAnimationStartDelay) + .setInterpolator(EMPHASIZED_ACCELERATE) + .withEndAction(() -> { + instantCollapse(); + mView.setAlpha(1f); + mView.setTranslationY(0f); + }) + .start(); + } + } + } + @VisibleForTesting void onFinishInflate() { loadDimens(); @@ -1139,7 +1012,7 @@ public final class NotificationPanelViewController { R.id.notification_stack_scroller); mNotificationStackScrollLayoutController.attach(stackScrollLayout); mNotificationStackScrollLayoutController.setOnHeightChangedListener( - mOnHeightChangedListener); + new NsslHeightChangedListener()); mNotificationStackScrollLayoutController.setOverscrollTopChangedListener( mOnOverscrollTopChangedListener); mNotificationStackScrollLayoutController.setOnScrollListener(this::onNotificationScrolled); @@ -1260,11 +1133,6 @@ public final class NotificationPanelViewController { } } - private void setCentralSurfaces(CentralSurfaces centralSurfaces) { - // TODO: this can be injected. - mCentralSurfaces = centralSurfaces; - } - public void updateResources() { mSplitShadeNotificationsScrimMarginBottom = mResources.getDimensionPixelSize( @@ -1350,7 +1218,7 @@ public final class NotificationPanelViewController { @VisibleForTesting void reInflateViews() { - if (DEBUG_LOGCAT) Log.d(TAG, "reInflateViews"); + debugLog("reInflateViews"); // Re-inflate the status view group. KeyguardStatusView keyguardStatusView = mNotificationContainerParent.findViewById(R.id.keyguard_status_view); @@ -1429,6 +1297,11 @@ public final class NotificationPanelViewController { mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView)); } + @VisibleForTesting + void setQs(QS qs) { + mQs = qs; + } + private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { mKeyguardMediaController.attachSplitShadeContainer(container); } @@ -1443,12 +1316,7 @@ public final class NotificationPanelViewController { } @VisibleForTesting - boolean getClosing() { - return mClosing; - } - - @VisibleForTesting - boolean getIsFlinging() { + boolean isFlinging() { return mIsFlinging; } @@ -1923,13 +1791,13 @@ public final class NotificationPanelViewController { setQsExpandImmediate(true); setShowShelfOnly(true); } - if (DEBUG) this.logf("collapse: " + this); + debugLog("collapse: %s", this); if (canPanelBeCollapsed()) { cancelHeightAnimator(); notifyExpandingStarted(); // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. - setIsClosing(true); + setClosing(true); if (delayed) { mNextCollapseSpeedUpFactor = speedUpFactor; this.mView.postDelayed(mFlingCollapseRunnable, 120); @@ -1939,13 +1807,19 @@ public final class NotificationPanelViewController { } } - private void setQsExpandImmediate(boolean expandImmediate) { + @VisibleForTesting + void setQsExpandImmediate(boolean expandImmediate) { if (expandImmediate != mQsExpandImmediate) { mQsExpandImmediate = expandImmediate; mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate); } } + @VisibleForTesting + boolean isQsExpandImmediate() { + return mQsExpandImmediate; + } + private void setShowShelfOnly(boolean shelfOnly) { mNotificationStackScrollLayoutController.setShouldShowShelfOnly( shelfOnly && !mSplitShadeEnabled); @@ -2032,12 +1906,12 @@ public final class NotificationPanelViewController { } } - public void fling(float vel, boolean expand) { + private void fling(float vel) { GestureRecorder gr = mCentralSurfaces.getGestureRecorder(); if (gr != null) { gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); } - fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); + fling(vel, true, 1.0f /* collapseSpeedUpFactor */, false); } @VisibleForTesting @@ -2124,7 +1998,7 @@ public final class NotificationPanelViewController { @Override public void onAnimationEnd(Animator animation) { if (shouldSpringBack && !mCancelled) { - // After the shade is flinged open to an overscrolled state, spring back + // After the shade is flung open to an overscrolled state, spring back // the shade by reducing section padding to 0. springBack(); } else { @@ -2154,7 +2028,7 @@ public final class NotificationPanelViewController { } private boolean onQsIntercept(MotionEvent event) { - if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept"); + debugLog("onQsIntercept"); int pointerIndex = event.findPointerIndex(mQsTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; @@ -2215,7 +2089,7 @@ public final class NotificationPanelViewController { if ((h > touchSlop || (h < -touchSlop && mQsExpanded)) && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { - if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion"); + debugLog("onQsIntercept - start tracking expansion"); mView.getParent().requestDisallowInterceptTouchEvent(true); mShadeLog.onQsInterceptMoveQsTrackingEnabled(h); mQsTracking = true; @@ -2274,7 +2148,7 @@ public final class NotificationPanelViewController { private void initDownStates(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mQsTouchAboveFalsingThreshold = mQsFullyExpanded; - mDozingOnDown = isDozing(); + mDozingOnDown = mDozing; mDownX = event.getX(); mDownY = event.getY(); mCollapsedOnDown = isFullyCollapsed(); @@ -2324,7 +2198,7 @@ public final class NotificationPanelViewController { float vel = getCurrentQSVelocity(); boolean expandsQs = flingExpandsQs(vel); if (expandsQs) { - if (mFalsingManager.isUnlockingDisabled() || isFalseTouch(QUICK_SETTINGS)) { + if (mFalsingManager.isUnlockingDisabled() || isFalseTouch()) { expandsQs = false; } else { logQsSwipeDown(y); @@ -2363,9 +2237,9 @@ public final class NotificationPanelViewController { } } - private boolean isFalseTouch(@Classifier.InteractionType int interactionType) { + private boolean isFalseTouch() { if (mFalsingManager.isClassifierEnabled()) { - return mFalsingManager.isFalseTouch(interactionType); + return mFalsingManager.isFalseTouch(Classifier.QUICK_SETTINGS); } return !mQsTouchAboveFalsingThreshold; } @@ -2491,7 +2365,7 @@ public final class NotificationPanelViewController { private void handleQsDown(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN && shouldQuickSettingsIntercept( event.getX(), event.getY(), -1)) { - if (DEBUG_LOGCAT) Log.d(TAG, "handleQsDown"); + debugLog("handleQsDown"); mFalsingCollector.onQsDown(); mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled"); mQsTracking = true; @@ -2505,9 +2379,7 @@ public final class NotificationPanelViewController { } } - /** - * Input focus transfer is about to happen. - */ + /** Input focus transfer is about to happen. */ public void startWaitingForOpenPanelGesture() { if (!isFullyCollapsed()) { return; @@ -2539,7 +2411,7 @@ public final class NotificationPanelViewController { } else { // Window never will receive touch events that typically trigger haptic on open. maybeVibrateOnOpening(false /* openingWithTouch */); - fling(velocity > 1f ? 1000f * velocity : 0, true /* expand */); + fling(velocity > 1f ? 1000f * velocity : 0 /* expand */); } onTrackingStopped(false); } @@ -2613,7 +2485,7 @@ public final class NotificationPanelViewController { break; case MotionEvent.ACTION_MOVE: - if (DEBUG_LOGCAT) Log.d(TAG, "onQSTouch move"); + debugLog("onQSTouch move"); mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion"); setQsExpansionHeight(h + mInitialHeightOnTouch); if (h >= getFalsingThreshold()) { @@ -2690,6 +2562,9 @@ public final class NotificationPanelViewController { navigationBarView.onStatusBarPanelStateChanged(); } mShadeExpansionStateManager.onQsExpansionChanged(expanded); + mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded, + mQsMinExpansionHeight, mQsMaxExpansionHeight, mStackScrollerOverscrolling, + mDozing, mQsAnimatorExpand, mAnimatingQS); } } @@ -2901,7 +2776,7 @@ public final class NotificationPanelViewController { } private int calculateLeftQsClippingBound() { - if (isFullWidth()) { + if (mIsFullWidth) { // left bounds can ignore insets, it should always reach the edge of the screen return 0; } else { @@ -2910,7 +2785,7 @@ public final class NotificationPanelViewController { } private int calculateRightQsClippingBound() { - if (isFullWidth()) { + if (mIsFullWidth) { return getView().getRight() + mDisplayRightInset; } else { return mNotificationStackScrollLayoutController.getRight(); @@ -2978,7 +2853,7 @@ public final class NotificationPanelViewController { // Fancy clipping for quick settings int radius = mScrimCornerRadius; boolean clipStatusView = false; - if (isFullWidth()) { + if (mIsFullWidth) { // The padding on this area is large enough that we can use a cheaper clipping strategy mKeyguardStatusAreaClipBounds.set(left, top, right, bottom); clipStatusView = qsVisible; @@ -3128,10 +3003,7 @@ public final class NotificationPanelViewController { } } - /** - * @return the topPadding of notifications when on keyguard not respecting quick settings - * expansion - */ + /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */ private int getKeyguardNotificationStaticPadding() { if (!mKeyguardShowing) { return 0; @@ -3163,7 +3035,7 @@ public final class NotificationPanelViewController { * shade. 0.0f means we're not transitioning yet. */ public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) { - if (animate && isFullWidth()) { + if (animate && mIsFullWidth) { animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, delay); mIsQsTranslationResetAnimator = mQsTranslationForFullShadeTransition > 0.0f; @@ -3212,10 +3084,7 @@ public final class NotificationPanelViewController { updateQsExpansion(); } - /** - * Notify the panel that the pulse expansion has finished and that we're going to the full - * shade - */ + /** Called when pulse expansion has finished and this is going to the full shade. */ public void onPulseExpansionFinished() { animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 0); mIsPulseExpansionResetAnimator = true; @@ -3270,9 +3139,7 @@ public final class NotificationPanelViewController { } } - /** - * @see #flingSettings(float, int, Runnable, boolean) - */ + /** @see #flingSettings(float, int, Runnable, boolean) */ public void flingSettings(float vel, int type) { flingSettings(vel, type, null /* onFinishRunnable */, false /* isClick */); } @@ -3405,7 +3272,8 @@ public final class NotificationPanelViewController { return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS); } - public int getMaxPanelHeight() { + @VisibleForTesting + int getMaxPanelHeight() { int min = mStatusBarMinHeight; if (!(mBarState == KEYGUARD) && mNotificationStackScrollLayoutController.getNotGoneChildCount() == 0) { @@ -3439,13 +3307,20 @@ public final class NotificationPanelViewController { } private void onHeightUpdated(float expandedHeight) { + if (expandedHeight <= 0) { + mShadeLog.logExpansionChanged("onHeightUpdated: fully collapsed.", + mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + } else if (isFullyExpanded()) { + mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.", + mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + } if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) { // Updating the clock position will set the top padding which might // trigger a new panel height and re-position the clock. // This is a circular dependency and should be avoided, otherwise we'll have // a stack overflow. if (mStackScrollerMeasuringPass > 2) { - if (DEBUG_LOGCAT) Log.d(TAG, "Unstable notification panel height. Aborting."); + debugLog("Unstable notification panel height. Aborting."); } else { positionClockAndNotifications(); } @@ -3581,9 +3456,7 @@ public final class NotificationPanelViewController { return alpha; } - /** - * Hides the header when notifications are colliding with it. - */ + /** Hides the header when notifications are colliding with it. */ private void updateHeader() { if (mBarState == KEYGUARD) { mKeyguardStatusBarViewController.updateViewState(); @@ -3726,7 +3599,7 @@ public final class NotificationPanelViewController { if (mAnimateAfterExpanding) { notifyExpandingStarted(); beginJankMonitoring(); - fling(0, true /* expand */); + fling(0 /* expand */); } else { setExpandedFraction(1f); } @@ -3763,6 +3636,24 @@ public final class NotificationPanelViewController { } + private void falsingAdditionalTapRequired() { + if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { + mTapAgainViewController.show(); + } else { + mKeyguardIndicationController.showTransientIndication( + R.string.notification_tap_again); + } + + if (!mStatusBarStateController.isDozing()) { + mVibratorHelper.vibrate( + Process.myUid(), + mView.getContext().getPackageName(), + ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT, + "falsing-additional-tap-required", + TOUCH_VIBRATION_ATTRIBUTES); + } + } + private void onTrackingStarted() { mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen()); endClosing(); @@ -3797,7 +3688,7 @@ public final class NotificationPanelViewController { private void updateMaxHeadsUpTranslation() { mNotificationStackScrollLayoutController.setHeadsUpBoundaries( - getHeight(), mNavigationBarBottomHeight); + mView.getHeight(), mNavigationBarBottomHeight); } @VisibleForTesting @@ -3842,7 +3733,8 @@ public final class NotificationPanelViewController { || !isTracking()); } - public int getMaxPanelTransitionDistance() { + @VisibleForTesting + int getMaxPanelTransitionDistance() { // Traditionally the value is based on the number of notifications. On split-shade, we want // the required distance to be a specific and constant value, to make sure the expansion // motion has the expected speed. We also only want this on non-lockscreen for now. @@ -3898,10 +3790,9 @@ public final class NotificationPanelViewController { } @VisibleForTesting - void setIsClosing(boolean isClosing) { - boolean wasClosing = isClosing(); - mClosing = isClosing; - if (wasClosing != isClosing) { + void setClosing(boolean isClosing) { + if (mClosing != isClosing) { + mClosing = isClosing; mShadeExpansionStateManager.notifyPanelCollapsingChanged(isClosing); } mAmbientState.setIsClosing(isClosing); @@ -3914,10 +3805,6 @@ public final class NotificationPanelViewController { } } - public boolean isDozing() { - return mDozing; - } - public void setQsScrimEnabled(boolean qsScrimEnabled) { boolean changed = mQsScrimEnabled != qsScrimEnabled; mQsScrimEnabled = qsScrimEnabled; @@ -3930,7 +3817,7 @@ public final class NotificationPanelViewController { mKeyguardStatusViewController.dozeTimeTick(); } - private boolean onMiddleClicked() { + private void onMiddleClicked() { switch (mBarState) { case KEYGUARD: if (!mDozingOnDown) { @@ -3952,14 +3839,12 @@ public final class NotificationPanelViewController { startUnlockHintAnimation(); } } - return true; + break; case StatusBarState.SHADE_LOCKED: if (!mQsExpanded) { mStatusBarStateController.setState(KEYGUARD); } - return true; - default: - return true; + break; } } @@ -4033,17 +3918,9 @@ public final class NotificationPanelViewController { updateStatusBarIcons(); } - /** - * @return whether the notifications are displayed full width and don't have any margins on - * the side. - */ - public boolean isFullWidth() { - return mIsFullWidth; - } - private void updateStatusBarIcons() { boolean showIconsWhenExpanded = - (isPanelVisibleBecauseOfHeadsUp() || isFullWidth()) + (isPanelVisibleBecauseOfHeadsUp() || mIsFullWidth) && getExpandedHeight() < getOpeningHeight(); if (showIconsWhenExpanded && isOnKeyguard()) { showIconsWhenExpanded = false; @@ -4058,10 +3935,7 @@ public final class NotificationPanelViewController { return mBarState == KEYGUARD; } - /** - * Called when heads-up notification is being dragged up or down to indicate what's the starting - * height for shade motion - */ + /** Called when a HUN is dragged up or down to indicate the starting height for shade motion. */ public void setHeadsUpDraggingStartingHeight(int startHeight) { mHeadsUpStartHeight = startHeight; float scrimMinFraction; @@ -4115,25 +3989,18 @@ public final class NotificationPanelViewController { setLaunchingAffordance(false); } - /** - * Set whether we are currently launching an affordance. This is currently only set when - * launched via a camera gesture. - */ + /** Set whether we are currently launching an affordance (i.e. camera gesture). */ private void setLaunchingAffordance(boolean launchingAffordance) { mLaunchingAffordance = launchingAffordance; mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); } - /** - * Return true when a bottom affordance is launching an occluded activity with a splash screen. - */ + /** Returns whether a bottom affordance is launching an occluded activity with splash screen. */ public boolean isLaunchingAffordanceWithPreview() { return mLaunchingAffordance; } - /** - * Whether the camera application can be launched for the camera launch gesture. - */ + /** Whether the camera application can be launched by the camera launch gesture. */ public boolean canCameraGestureBeLaunched() { return mCameraGestureHelper.canCameraGestureBeLaunched(mBarState); } @@ -4146,22 +4013,19 @@ public final class NotificationPanelViewController { && mHeadsUpAppearanceController.shouldBeVisible()) { return false; } - return !isFullWidth() || !mShowIconsWhenExpanded; + return !mIsFullWidth || !mShowIconsWhenExpanded; } - public final QS.ScrollListener mScrollListener = new QS.ScrollListener() { - @Override - public void onQsPanelScrollChanged(int scrollY) { - mLargeScreenShadeHeaderController.setQsScrollY(scrollY); - if (scrollY > 0 && !mQsFullyExpanded) { - if (DEBUG_LOGCAT) Log.d(TAG, "Scrolling while not expanded. Forcing expand"); - // If we are scrolling QS, we should be fully expanded. - expandWithQs(); - } + private void onQsPanelScrollChanged(int scrollY) { + mLargeScreenShadeHeaderController.setQsScrollY(scrollY); + if (scrollY > 0 && !mQsFullyExpanded) { + debugLog("Scrolling while not expanded. Forcing expand"); + // If we are scrolling QS, we should be fully expanded. + expandWithQs(); } - }; + } - private final FragmentListener mFragmentListener = new FragmentListener() { + private final class QsFragmentListener implements FragmentListener { @Override public void onFragmentViewCreated(String tag, Fragment fragment) { mQs = (QS) fragment; @@ -4178,7 +4042,7 @@ public final class NotificationPanelViewController { final int height = bottom - top; final int oldHeight = oldBottom - oldTop; if (height != oldHeight) { - mHeightListener.onQsHeightChanged(); + onQsHeightChanged(); } }); mQs.setCollapsedMediaVisibilityChangedListener((visible) -> { @@ -4191,7 +4055,7 @@ public final class NotificationPanelViewController { mLockscreenShadeTransitionController.setQS(mQs); mShadeTransitionController.setQs(mQs); mNotificationStackScrollLayoutController.setQsHeader((ViewGroup) mQs.getHeader()); - mQs.setScrollListener(mScrollListener); + mQs.setScrollListener(mQsScrollListener); updateQsExpansion(); } @@ -4204,7 +4068,7 @@ public final class NotificationPanelViewController { mQs = null; } } - }; + } private void animateNextNotificationBounds(long duration, long delay) { mAnimateNextNotificationBounds = true; @@ -4294,13 +4158,7 @@ public final class NotificationPanelViewController { mKeyguardStatusViewController.setStatusAccessibilityImportance(mode); } - /** - * TODO: this should be removed. - * It's not correct to pass this view forward because other classes will end up adding - * children to it. Theme will be out of sync. - * - * @return bottom area view - */ + //TODO(b/254875405): this should be removed. public KeyguardBottomAreaView getKeyguardBottomAreaView() { return mKeyguardBottomArea; } @@ -4329,11 +4187,8 @@ public final class NotificationPanelViewController { mHeadsUpAppearanceController = headsUpAppearanceController; } - /** - * Starts the animation before we dismiss Keyguard, i.e. an disappearing animation on the - * security view of the bouncer. - */ - public void onBouncerPreHideAnimation() { + /** Called before animating Keyguard dismissal, i.e. the animation dismissing the bouncer. */ + public void startBouncerPreHideAnimation() { if (mKeyguardQsUserSwitchController != null) { mKeyguardQsUserSwitchController.setKeyguardQsUserSwitchVisibility( mBarState, @@ -4350,9 +4205,7 @@ public final class NotificationPanelViewController { } } - /** - * Updates the views to the initial state for the fold to AOD animation - */ + /** Updates the views to the initial state for the fold to AOD animation. */ public void prepareFoldToAodAnimation() { // Force show AOD UI even if we are not locked showAodUi(); @@ -4394,14 +4247,11 @@ public final class NotificationPanelViewController { public void onAnimationEnd(Animator animation) { endAction.run(); } - }).setUpdateListener(anim -> { - mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); - }).start(); + }).setUpdateListener(anim -> mKeyguardStatusViewController.animateFoldToAod( + anim.getAnimatedFraction())).start(); } - /** - * Cancels fold to AOD transition and resets view state - */ + /** Cancels fold to AOD transition and resets view state. */ public void cancelFoldToAodAnimation() { cancelAnimation(); resetAlpha(); @@ -4445,42 +4295,11 @@ public final class NotificationPanelViewController { } } - public boolean hasActiveClearableNotifications() { - return mNotificationStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL); - } public RemoteInputController.Delegate createRemoteInputDelegate() { return mNotificationStackScrollLayoutController.createDelegate(); } - /** - * Updates the notification views' sections and status bar icons. This is - * triggered by the NotificationPresenter whenever there are changes to the underlying - * notification data being displayed. In the new notification pipeline, this is handled in - * {@link ShadeViewManager}. - */ - public void updateNotificationViews() { - mNotificationStackScrollLayoutController.updateFooter(); - - mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList()); - } - - private List<ListEntry> createVisibleEntriesList() { - List<ListEntry> entries = new ArrayList<>( - mNotificationStackScrollLayoutController.getChildCount()); - for (int i = 0; i < mNotificationStackScrollLayoutController.getChildCount(); i++) { - View view = mNotificationStackScrollLayoutController.getChildAt(i); - if (view instanceof ExpandableNotificationRow) { - entries.add(((ExpandableNotificationRow) view).getEntry()); - } - } - return entries; - } - - public void onUpdateRowStates() { - mNotificationStackScrollLayoutController.onUpdateRowStates(); - } - public boolean hasPulsingNotifications() { return mNotificationListContainer.hasPulsingNotifications(); } @@ -4497,16 +4316,6 @@ public final class NotificationPanelViewController { mNotificationStackScrollLayoutController.runAfterAnimationFinished(r); } - private Runnable mHideExpandedRunnable; - private final Runnable mMaybeHideExpandedRunnable = new Runnable() { - @Override - public void run() { - if (getExpansionFraction() == 0.0f) { - mView.post(mHideExpandedRunnable); - } - } - }; - /** * Initialize objects instead of injecting to avoid circular dependencies. * @@ -4516,7 +4325,9 @@ public final class NotificationPanelViewController { CentralSurfaces centralSurfaces, Runnable hideExpandedRunnable, NotificationShelfController notificationShelfController) { - setCentralSurfaces(centralSurfaces); + // TODO(b/254859580): this can be injected. + mCentralSurfaces = centralSurfaces; + mHideExpandedRunnable = hideExpandedRunnable; mNotificationStackScrollLayoutController.setShelfController(notificationShelfController); mNotificationShelfController = notificationShelfController; @@ -4524,10 +4335,6 @@ public final class NotificationPanelViewController { updateMaxDisplayedNotifications(true); } - public void setAlpha(float alpha) { - mView.setAlpha(alpha); - } - public void resetTranslation() { mView.setTranslationX(0f); } @@ -4546,22 +4353,18 @@ public final class NotificationPanelViewController { ViewGroupFadeHelper.reset(mView); } - public void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) { + void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) { mView.getViewTreeObserver().addOnGlobalLayoutListener(listener); } - public void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) { + void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) { mView.getViewTreeObserver().removeOnGlobalLayoutListener(listener); } - public MyOnHeadsUpChangedListener getOnHeadsUpChangedListener() { + public ShadeHeadsUpChangedListener getOnHeadsUpChangedListener() { return mOnHeadsUpChangedListener; } - public int getHeight() { - return mView.getHeight(); - } - public void setHeaderDebugInfo(String text) { if (DEBUG_DRAWABLE) mHeaderDebugInfo = text; } @@ -4570,10 +4373,6 @@ public final class NotificationPanelViewController { mConfigurationListener.onThemeChanged(); } - private OnLayoutChangeListener createLayoutChangeListener() { - return new OnLayoutChangeListener(); - } - @VisibleForTesting TouchHandler createTouchHandler() { return new TouchHandler(); @@ -4628,10 +4427,6 @@ public final class NotificationPanelViewController { } }; - private OnConfigurationChangedListener createOnConfigurationChangedListener() { - return new OnConfigurationChangedListener(); - } - public NotificationStackScrollLayoutController getNotificationStackScrollLayoutController() { return mNotificationStackScrollLayoutController; } @@ -4672,13 +4467,7 @@ public final class NotificationPanelViewController { ); } - private void unregisterSettingsChangeListener() { - mContentResolver.unregisterContentObserver(mSettingsChangeObserver); - } - - /** - * Updates notification panel-specific flags on {@link SysUiState}. - */ + /** Updates notification panel-specific flags on {@link SysUiState}. */ public void updateSystemUiStateFlags() { if (SysUiState.DEBUG) { Log.d(TAG, "Updating panel sysui state flags: fullyExpanded=" @@ -4690,8 +4479,10 @@ public final class NotificationPanelViewController { .commitUpdate(mDisplayId); } - private void logf(String fmt, Object... args) { - Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); + private void debugLog(String fmt, Object... args) { + if (DEBUG_LOGCAT) { + Log.d(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); + } } @VisibleForTesting @@ -4755,9 +4546,8 @@ public final class NotificationPanelViewController { * Maybe vibrate as panel is opened. * * @param openingWithTouch Whether the panel is being opened with touch. If the panel is - * instead - * being opened programmatically (such as by the open panel gesture), we - * always play haptic. + * instead being opened programmatically (such as by the open panel + * gesture), we always play haptic. */ private void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening) { @@ -4856,8 +4646,8 @@ public final class NotificationPanelViewController { } else if (!mCentralSurfaces.isBouncerShowing() && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() && !mKeyguardStateController.isKeyguardGoingAway()) { - boolean expands = onEmptySpaceClick(); - onTrackingStopped(expands); + onEmptySpaceClick(); + onTrackingStopped(true); } mVelocityTracker.clear(); } @@ -4869,7 +4659,7 @@ public final class NotificationPanelViewController { private void endClosing() { if (mClosing) { - setIsClosing(false); + setClosing(false); onClosingFinished(); } } @@ -4904,7 +4694,7 @@ public final class NotificationPanelViewController { boolean expandBecauseOfFalsing) { float target = expand ? getMaxPanelHeight() : 0; if (!expand) { - setIsClosing(true); + setClosing(true); } flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); } @@ -4939,13 +4729,9 @@ public final class NotificationPanelViewController { animator.start(); } - public String getName() { - return mViewName; - } - @VisibleForTesting void setExpandedHeight(float height) { - if (DEBUG) logf("setExpandedHeight(%.1f)", height); + debugLog("setExpandedHeight(%.1f)", height); setExpandedHeightInternal(height); } @@ -5037,7 +4823,7 @@ public final class NotificationPanelViewController { return mExpandedHeight; } - public float getExpandedFraction() { + private float getExpandedFraction() { return mExpandedFraction; } @@ -5053,10 +4839,6 @@ public final class NotificationPanelViewController { return mClosing || mIsLaunchAnimationRunning; } - public boolean isFlinging() { - return mIsFlinging; - } - public boolean isTracking() { return mTracking; } @@ -5203,8 +4985,7 @@ public final class NotificationPanelViewController { */ public void updatePanelExpansionAndVisibility() { mShadeExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), - mTracking, mExpansionDragDownAmountPx); + mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); updateVisibility(); } @@ -5217,16 +4998,11 @@ public final class NotificationPanelViewController { && !mIsSpringBackAnimation; } - /** - * Gets called when the user performs a click anywhere in the empty area of the panel. - * - * @return whether the panel will be expanded after the action performed by this method - */ - private boolean onEmptySpaceClick() { - if (mHintAnimationRunning) { - return true; + /** Called when the user performs a click anywhere in the empty area of the panel. */ + private void onEmptySpaceClick() { + if (!mHintAnimationRunning) { + onMiddleClicked(); } - return onMiddleClicked(); } @VisibleForTesting @@ -5243,7 +5019,7 @@ public final class NotificationPanelViewController { /** Returns the NotificationPanelView. */ public ViewGroup getView() { - // TODO: remove this method, or at least reduce references to it. + // TODO(b/254878364): remove this method, or at least reduce references to it. return mView; } @@ -5283,12 +5059,11 @@ public final class NotificationPanelViewController { return mShadeExpansionStateManager; } - private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener { + private final class NsslHeightChangedListener implements + ExpandableView.OnHeightChangedListener { @Override public void onHeightChanged(ExpandableView view, boolean needsAnimation) { - - // Block update if we are in quick settings and just the top padding changed - // (i.e. view == null). + // Block update if we are in QS and just the top padding changed (i.e. view == null). if (view == null && mQsExpanded) { return; } @@ -5312,26 +5087,22 @@ public final class NotificationPanelViewController { } @Override - public void onReset(ExpandableView view) { - } + public void onReset(ExpandableView view) {} } - private class CollapseExpandAction implements Runnable { - @Override - public void run() { - onQsExpansionStarted(); - if (mQsExpanded) { - flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */, - true /* isClick */); - } else if (isQsExpansionEnabled()) { - mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0); - flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */, - true /* isClick */); - } + private void collapseOrExpand() { + onQsExpansionStarted(); + if (mQsExpanded) { + flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */, + true /* isClick */); + } else if (isQsExpansionEnabled()) { + mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0); + flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */, + true /* isClick */); } } - private class OnOverscrollTopChangedListener implements + private final class NsslOverscrollTopChangedListener implements NotificationStackScrollLayout.OnOverscrollTopChangedListener { @Override public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { @@ -5375,27 +5146,16 @@ public final class NotificationPanelViewController { } } - private class DynamicPrivacyControlListener implements DynamicPrivacyController.Listener { - @Override - public void onDynamicPrivacyChanged() { - // Do not request animation when pulsing or waking up, otherwise the clock wiill be out - // of sync with the notification panel. - if (mLinearDarkAmount != 0) { - return; - } - mAnimateNextPositionUpdate = true; - } - } - - private class OnEmptySpaceClickListener implements - NotificationStackScrollLayout.OnEmptySpaceClickListener { - @Override - public void onEmptySpaceClicked(float x, float y) { - onEmptySpaceClick(); + private void onDynamicPrivacyChanged() { + // Do not request animation when pulsing or waking up, otherwise the clock will be out + // of sync with the notification panel. + if (mLinearDarkAmount != 0) { + return; } + mAnimateNextPositionUpdate = true; } - private class MyOnHeadsUpChangedListener implements OnHeadsUpChangedListener { + private final class ShadeHeadsUpChangedListener implements OnHeadsUpChangedListener { @Override public void onHeadsUpPinnedModeChanged(final boolean inPinnedMode) { if (inPinnedMode) { @@ -5435,32 +5195,31 @@ public final class NotificationPanelViewController { } } - private class HeightListener implements QS.HeightListener { - public void onQsHeightChanged() { - mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0; - if (mQsExpanded && mQsFullyExpanded) { - mQsExpansionHeight = mQsMaxExpansionHeight; - requestScrollerTopPaddingUpdate(false /* animate */); - updateExpandedHeightToMaxHeight(); - } - if (mAccessibilityManager.isEnabled()) { - mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); - } - mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight); + private void onQsHeightChanged() { + mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0; + if (mQsExpanded && mQsFullyExpanded) { + mQsExpansionHeight = mQsMaxExpansionHeight; + requestScrollerTopPaddingUpdate(false /* animate */); + updateExpandedHeightToMaxHeight(); + } + if (mAccessibilityManager.isEnabled()) { + mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); } + mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight); } - private class ConfigurationListener implements ConfigurationController.ConfigurationListener { + private final class ConfigurationListener implements + ConfigurationController.ConfigurationListener { @Override public void onThemeChanged() { - if (DEBUG_LOGCAT) Log.d(TAG, "onThemeChanged"); + debugLog("onThemeChanged"); reInflateViews(); } @Override public void onSmallestScreenWidthChanged() { Trace.beginSection("onSmallestScreenWidthChanged"); - if (DEBUG_LOGCAT) Log.d(TAG, "onSmallestScreenWidthChanged"); + debugLog("onSmallestScreenWidthChanged"); // Can affect multi-user switcher visibility as it depends on screen size by default: // it is enabled only for devices with large screens (see config_keyguardUserSwitcher) @@ -5477,27 +5236,26 @@ public final class NotificationPanelViewController { @Override public void onDensityOrFontScaleChanged() { - if (DEBUG_LOGCAT) Log.d(TAG, "onDensityOrFontScaleChanged"); + debugLog("onDensityOrFontScaleChanged"); reInflateViews(); } } - private class SettingsChangeObserver extends ContentObserver { - + private final class SettingsChangeObserver extends ContentObserver { SettingsChangeObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange) { - if (DEBUG_LOGCAT) Log.d(TAG, "onSettingsChanged"); + debugLog("onSettingsChanged"); // Can affect multi-user switcher visibility reInflateViews(); } } - private class StatusBarStateListener implements StateListener { + private final class StatusBarStateListener implements StateListener { @Override public void onStateChanged(int statusBarState) { boolean goingToFullShade = mStatusBarStateController.goingToFullShade(); @@ -5653,21 +5411,19 @@ public final class NotificationPanelViewController { setExpandedFraction(1f); } - /** - * Sets the overstretch amount in raw pixels when dragging down. - */ - public void setOverStrechAmount(float amount) { + /** Sets the overstretch amount in raw pixels when dragging down. */ + public void setOverStretchAmount(float amount) { float progress = amount / mView.getHeight(); - float overstretch = Interpolators.getOvershootInterpolation(progress); - mOverStretchAmount = overstretch * mMaxOverscrollAmountForPulse; + float overStretch = Interpolators.getOvershootInterpolation(progress); + mOverStretchAmount = overStretch * mMaxOverscrollAmountForPulse; positionClockAndNotifications(true /* forceUpdate */); } - private class OnAttachStateChangeListener implements View.OnAttachStateChangeListener { + private final class ShadeAttachStateChangeListener implements View.OnAttachStateChangeListener { @Override public void onViewAttachedToWindow(View v) { mFragmentService.getFragmentHostManager(mView) - .addTagListener(QS.TAG, mFragmentListener); + .addTagListener(QS.TAG, mQsFragmentListener); mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState()); mConfigurationController.addCallback(mConfigurationListener); @@ -5682,16 +5438,16 @@ public final class NotificationPanelViewController { @Override public void onViewDetachedFromWindow(View v) { - unregisterSettingsChangeListener(); + mContentResolver.unregisterContentObserver(mSettingsChangeObserver); mFragmentService.getFragmentHostManager(mView) - .removeTagListener(QS.TAG, mFragmentListener); + .removeTagListener(QS.TAG, mQsFragmentListener); mStatusBarStateController.removeCallback(mStatusBarStateListener); mConfigurationController.removeCallback(mConfigurationListener); mFalsingManager.removeTapListener(mFalsingTapListener); } } - private final class OnLayoutChangeListener implements View.OnLayoutChangeListener { + private final class ShadeLayoutChangeListener implements View.OnLayoutChangeListener { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { @@ -5700,7 +5456,7 @@ public final class NotificationPanelViewController { mHasLayoutedSinceDown = true; if (mUpdateFlingOnLayout) { abortAnimations(); - fling(mUpdateFlingVelocity, true /* expands */); + fling(mUpdateFlingVelocity); mUpdateFlingOnLayout = false; } updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount()); @@ -5734,14 +5490,11 @@ public final class NotificationPanelViewController { updateExpandedHeight(getExpandedHeight()); updateHeader(); - // If we are running a size change animation, the animation takes care of the height of - // the container. However, if we are not animating, we always need to make the QS - // container - // the desired height so when closing the QS detail, it stays smaller after the size - // change - // animation is finished but the detail view is still being animated away (this - // animation - // takes longer than the size change animation). + // If we are running a size change animation, the animation takes care of the height + // of the container. However, if we are not animating, we always need to make the QS + // container the desired height so when closing the QS detail, it stays smaller after + // the size change animation is finished but the detail view is still being animated + // away (this animation takes longer than the size change animation). if (mQsSizeChangeAnimator == null && mQs != null) { mQs.setHeightOverride(mQs.getDesiredHeight()); } @@ -5767,13 +5520,12 @@ public final class NotificationPanelViewController { } } - private class DebugDrawable extends Drawable { - + private final class DebugDrawable extends Drawable { private final Set<Integer> mDebugTextUsedYPositions = new HashSet<>(); private final Paint mDebugPaint = new Paint(); @Override - public void draw(@androidx.annotation.NonNull @NonNull Canvas canvas) { + public void draw(@NonNull Canvas canvas) { mDebugTextUsedYPositions.clear(); mDebugPaint.setColor(Color.RED); @@ -5851,18 +5603,17 @@ public final class NotificationPanelViewController { } } - private class OnApplyWindowInsetsListener implements View.OnApplyWindowInsetsListener { - public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - // the same types of insets that are handled in NotificationShadeWindowView - int insetTypes = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout(); - Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes); - mDisplayTopInset = combinedInsets.top; - mDisplayRightInset = combinedInsets.right; + @NonNull + private WindowInsets onApplyShadeWindowInsets(WindowInsets insets) { + // the same types of insets that are handled in NotificationShadeWindowView + int insetTypes = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout(); + Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes); + mDisplayTopInset = combinedInsets.top; + mDisplayRightInset = combinedInsets.right; - mNavigationBarBottomHeight = insets.getStableInsetBottom(); - updateMaxHeadsUpTranslation(); - return insets; - } + mNavigationBarBottomHeight = insets.getStableInsetBottom(); + updateMaxHeadsUpTranslation(); + return insets; } /** Removes any pending runnables that would collapse the panel. */ @@ -5870,9 +5621,6 @@ public final class NotificationPanelViewController { mView.removeCallbacks(mMaybeHideExpandedRunnable); } - @PanelState - private int mCurrentPanelState = STATE_CLOSED; - private void onPanelStateChanged(@PanelState int state) { updateQSExpansionEnabledAmbient(); @@ -5909,6 +5657,11 @@ public final class NotificationPanelViewController { } @VisibleForTesting + StateListener getStatusBarStateListener() { + return mStatusBarStateListener; + } + + @VisibleForTesting boolean isHintAnimationRunning() { return mHintAnimationRunning; } @@ -5956,7 +5709,7 @@ public final class NotificationPanelViewController { } if (!isFullyCollapsed() && onQsIntercept(event)) { - if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true"); + debugLog("onQsIntercept true"); return true; } if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted @@ -6159,7 +5912,6 @@ public final class NotificationPanelViewController { * * Flinging is also enabled in order to open or close the shade. */ - int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; @@ -6175,6 +5927,7 @@ public final class NotificationPanelViewController { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + mShadeLog.logMotionEvent(event, "onTouch: down action"); startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); mMinExpandHeight = 0.0f; mPanelClosedOnDown = isFullyCollapsed(); @@ -6263,6 +6016,7 @@ public final class NotificationPanelViewController { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: + mShadeLog.logMotionEvent(event, "onTouch: up/cancel action"); addMovement(event); endMotionEvent(event, x, y, false /* forceCancel */); // mHeightAnimator is null, there is no remaining frame, ends instrumenting. @@ -6279,15 +6033,6 @@ public final class NotificationPanelViewController { } } - /** Listens for config changes. */ - public class OnConfigurationChangedListener implements - NotificationPanelView.OnConfigurationChangedListener { - @Override - public void onConfigurationChanged(Configuration newConfig) { - loadDimens(); - } - } - static class SplitShadeTransitionAdapter extends Transition { private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; @@ -6337,5 +6082,27 @@ public final class NotificationPanelViewController { return TRANSITION_PROPERTIES; } } + + private final class ShadeAccessibilityDelegate extends AccessibilityDelegate { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action + == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId() + || action + == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) { + mStatusBarKeyguardViewManager.showBouncer(true); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 2b788d85a14c..7f1bba350af1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -77,4 +77,50 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } ) } + + fun logExpansionChanged( + message: String, + fraction: Float, + expanded: Boolean, + tracking: Boolean, + dragDownPxAmount: Float, + ) { + log(LogLevel.VERBOSE, { + str1 = message + double1 = fraction.toDouble() + bool1 = expanded + bool2 = tracking + long1 = dragDownPxAmount.toLong() + }, { + "$str1 fraction=$double1,expanded=$bool1," + + "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount" + }) + } + + fun logQsExpansionChanged( + message: String, + qsExpanded: Boolean, + qsMinExpansionHeight: Int, + qsMaxExpansionHeight: Int, + stackScrollerOverscrolling: Boolean, + dozing: Boolean, + qsAnimatorExpand: Boolean, + animatingQs: Boolean + ) { + log(LogLevel.VERBOSE, { + str1 = message + bool1 = qsExpanded + int1 = qsMinExpansionHeight + int2 = qsMaxExpansionHeight + bool2 = stackScrollerOverscrolling + bool3 = dozing + bool4 = qsAnimatorExpand + // 0 = false, 1 = true + long1 = animatingQs.compareTo(false).toLong() + }, { + "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + + "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," + + "animatingQs=$long1" + }) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index a2e4536ce45f..b8302d706e8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -663,7 +663,7 @@ class LockscreenShadeTransitionController @Inject constructor( } else { pulseHeight = height val overflow = nsslController.setPulseHeight(height) - notificationPanelController.setOverStrechAmount(overflow) + notificationPanelController.setOverStretchAmount(overflow) val transitionHeight = if (keyguardBypassController.bypassEnabled) height else 0.0f transitionToShadeAmountCommon(transitionHeight) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8a31ed9271ad..470cbcb842c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -198,6 +198,13 @@ class HeadsUpCoordinator @Inject constructor( // At this point we just need to initiate the transfer val summaryUpdate = mPostedEntries[logicalSummary.key] + // Because we now know for certain that some child is going to alert for this summary + // (as we have found a child to transfer the alert to), mark the group as having + // interrupted. This will allow us to know in the future that the "should heads up" + // state of this group has already been handled, just not via the summary entry itself. + logicalSummary.setInterruption() + mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentAlert.key) + // If the summary was not attached, then remove the alert from the detached summary. // Otherwise we can simply ignore its posted update. if (!isSummaryAttached) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index dfaa291c6bb6..473c35d6095a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -69,4 +69,13 @@ class HeadsUpCoordinatorLogger constructor( "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1" }) } + + fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = summaryKey + str2 = childKey + }, { + "marked group summary as interrupted: $str1 for alert transfer to child: $str2" + }) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index d227ed366ecb..eb7a7427e07a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -3468,10 +3468,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNavigationBarController.showPinningEscapeToast(mDisplayId); } - /** - * TODO: Remove this method. Views should not be passed forward. Will cause theme issues. - * @return bottom area view - */ + //TODO(b/254875405): this should be removed. @Override public KeyguardBottomAreaView getKeyguardBottomAreaView() { return mNotificationPanelViewController.getKeyguardBottomAreaView(); @@ -4184,7 +4181,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { */ @Override public void onBouncerPreHideAnimation() { - mNotificationPanelViewController.onBouncerPreHideAnimation(); + mNotificationPanelViewController.startBouncerPreHideAnimation(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt new file mode 100644 index 000000000000..e61890523ebb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt @@ -0,0 +1,27 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.data.model + +import android.net.NetworkCapabilities + +/** Provides information about a mobile network connection */ +data class MobileConnectivityModel( + /** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */ + val isConnected: Boolean = false, + /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */ + val isValidated: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 06e8f467ee0b..581842bc2f57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -16,11 +16,15 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.content.Context +import android.database.ContentObserver +import android.provider.Settings.Global import android.telephony.CellSignalStrength import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE @@ -34,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import com.android.systemui.util.settings.GlobalSettings import java.lang.IllegalStateException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -42,9 +47,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** @@ -65,14 +73,23 @@ interface MobileConnectionRepository { */ val subscriptionModelFlow: Flow<MobileSubscriptionModel> /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ - val dataEnabled: Flow<Boolean> + val dataEnabled: StateFlow<Boolean> + /** + * True if this connection represents the default subscription per + * [SubscriptionManager.getDefaultDataSubscriptionId] + */ + val isDefaultDataSubscription: StateFlow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( + private val context: Context, private val subId: Int, private val telephonyManager: TelephonyManager, + private val globalSettings: GlobalSettings, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, scope: CoroutineScope, @@ -86,6 +103,8 @@ class MobileConnectionRepositoryImpl( } } + private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { var state = MobileSubscriptionModel() conflatedCallbackFlow { @@ -165,33 +184,75 @@ class MobileConnectionRepositoryImpl( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .onEach { telephonyCallbackEvent.tryEmit(Unit) } .logOutputChange(logger, "MobileSubscriptionModel") .stateIn(scope, SharingStarted.WhileSubscribed(), state) } + /** Produces whenever the mobile data setting changes for this subId */ + private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"), + /* notifyForDescendants */ true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + /** * There are a few cases where we will need to poll [TelephonyManager] so we can update some * internal state where callbacks aren't provided. Any of those events should be merged into * this flow, which can be used to trigger the polling. */ - private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {} + private val telephonyPollingEvent: Flow<Unit> = + merge( + telephonyCallbackEvent, + localMobileDataSettingChangedEvent, + globalMobileDataSettingChangedEvent, + ) - override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() } + override val dataEnabled: StateFlow<Boolean> = + telephonyPollingEvent + .mapLatest { dataConnectionAllowed() } + .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + override val isDefaultDataSubscription: StateFlow<Boolean> = + defaultDataSubId + .mapLatest { it == subId } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) + class Factory @Inject constructor( + private val context: Context, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, + private val globalSettings: GlobalSettings, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { - fun build(subId: Int): MobileConnectionRepository { + fun build( + subId: Int, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( + context, subId, telephonyManager.createForSubscriptionId(subId), + globalSettings, + defaultDataSubId, + globalMobileDataSettingChangedEvent, bgDispatcher, logger, scope, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 0e2428ae393a..c3c1f1403c60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -16,15 +16,27 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.annotation.SuppressLint import android.content.Context import android.content.IntentFilter +import android.database.ContentObserver +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.provider.Settings +import android.provider.Settings.Global.MOBILE_DATA import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.annotation.VisibleForTesting +import com.android.internal.telephony.PhoneConstants import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config import com.android.systemui.broadcast.BroadcastDispatcher @@ -32,7 +44,9 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -40,10 +54,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -57,13 +73,22 @@ interface MobileConnectionsRepository { val subscriptionsFlow: Flow<List<SubscriptionInfo>> /** Observable for the subscriptionId of the current mobile data connection */ - val activeMobileDataSubscriptionId: Flow<Int> + val activeMobileDataSubscriptionId: StateFlow<Int> /** Observable for [MobileMappings.Config] tracking the defaults */ val defaultDataSubRatConfig: StateFlow<Config> + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ + val defaultDataSubId: StateFlow<Int> + + /** The current connectivity status for the default mobile network connection */ + val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> + /** Get or create a repository for the line of service for the given subscription ID */ fun getRepoForSubId(subId: Int): MobileConnectionRepository + + /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */ + val globalMobileDataSettingChangedEvent: Flow<Unit> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @@ -72,10 +97,12 @@ interface MobileConnectionsRepository { class MobileConnectionsRepositoryImpl @Inject constructor( + private val connectivityManager: ConnectivityManager, private val subscriptionManager: SubscriptionManager, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, broadcastDispatcher: BroadcastDispatcher, + private val globalSettings: GlobalSettings, private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @@ -121,17 +148,26 @@ constructor( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) + + private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow<Int> = + broadcastDispatcher + .broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) { intent, _ -> + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + } + .distinctUntilChanged() + .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } .stateIn( scope, - started = SharingStarted.WhileSubscribed(), - SubscriptionManager.INVALID_SUBSCRIPTION_ID + SharingStarted.WhileSubscribed(), + SubscriptionManager.getDefaultDataSubscriptionId() ) - private val defaultDataSubChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - ) - private val carrierConfigChangedEvent = broadcastDispatcher.broadcastFlow( IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) @@ -148,9 +184,8 @@ constructor( * This flow will produce whenever the default data subscription or the carrier config changes. */ override val defaultDataSubRatConfig: StateFlow<Config> = - combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> - Config.readConfig(context) - } + merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) + .mapLatest { Config.readConfig(context) } .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -168,6 +203,57 @@ constructor( ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } } + /** + * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual + * connection repositories also observe the URI for [MOBILE_DATA] + subId. + */ + override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + + @SuppressLint("MissingPermission") + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = + conflatedCallbackFlow { + val callback = + object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onLost(network: Network) { + // Send a disconnected model when lost. Maybe should create a sealed + // type or null here? + trySend(MobileConnectivityModel()) + } + + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities + ) { + trySend( + MobileConnectivityModel( + isConnected = caps.hasTransport(TRANSPORT_CELLULAR), + isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED), + ) + ) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) + private fun isValidSubId(subId: Int): Boolean { subscriptionsFlow.value.forEach { if (it.subscriptionId == subId) { @@ -181,7 +267,11 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build(subId) + return mobileConnectionRepositoryFactory.build( + subId, + defaultDataSubId, + globalMobileDataSettingChangedEvent, + ) } private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt index 77de849691db..91886bb121d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapLatest @@ -40,7 +39,7 @@ import kotlinx.coroutines.withContext */ interface UserSetupRepository { /** Observable tracking [DeviceProvisionedController.isUserSetup] */ - val isUserSetupFlow: Flow<Boolean> + val isUserSetupFlow: StateFlow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index f99d278c3903..0da84f0bec9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -18,81 +18,109 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn interface MobileIconInteractor { + /** Only true if mobile is the default transport but is not validated, otherwise false */ + val isDefaultConnectionFailed: StateFlow<Boolean> + + /** True when telephony tells us that the data state is CONNECTED */ + val isDataConnected: StateFlow<Boolean> + + // TODO(b/256839546): clarify naming of default vs active + /** True if we want to consider the data connection enabled */ + val isDefaultDataEnabled: StateFlow<Boolean> + /** Observable for the data enabled state of this connection */ - val isDataEnabled: Flow<Boolean> + val isDataEnabled: StateFlow<Boolean> /** Observable for RAT type (network type) indicator */ - val networkTypeIconGroup: Flow<MobileIconGroup> + val networkTypeIconGroup: StateFlow<MobileIconGroup> /** True if this line of service is emergency-only */ - val isEmergencyOnly: Flow<Boolean> + val isEmergencyOnly: StateFlow<Boolean> /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ - val level: Flow<Int> + val level: StateFlow<Int> /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ - val numberOfLevels: Flow<Int> - - /** True when we want to draw an icon that makes room for the exclamation mark */ - val cutOut: Flow<Boolean> + val numberOfLevels: StateFlow<Int> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) class MobileIconInteractorImpl( - defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, - defaultMobileIconGroup: Flow<MobileIconGroup>, + @Application scope: CoroutineScope, + defaultSubscriptionHasDataEnabled: StateFlow<Boolean>, + defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: StateFlow<MobileIconGroup>, + override val isDefaultConnectionFailed: StateFlow<Boolean>, mobileMappingsProxy: MobileMappingsProxy, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { private val mobileStatusInfo = connectionRepository.subscriptionModelFlow - override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled + override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled + + override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ - override val networkTypeIconGroup: Flow<MobileIconGroup> = + override val networkTypeIconGroup: StateFlow<MobileIconGroup> = combine( - mobileStatusInfo, - defaultMobileIconMapping, - defaultMobileIconGroup, - ) { info, mapping, defaultGroup -> - val lookupKey = - when (val resolved = info.resolvedNetworkType) { - is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) - is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> + mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup + } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) + + override val isEmergencyOnly: StateFlow<Boolean> = + mobileStatusInfo + .mapLatest { it.isEmergencyOnly } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val level: StateFlow<Int> = + mobileStatusInfo + .mapLatest { mobileModel -> + // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] + if (mobileModel.isGsm) { + mobileModel.primaryLevel + } else { + mobileModel.cdmaLevel } - mapping[lookupKey] ?: defaultGroup - } - - override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } - - override val level: Flow<Int> = - mobileStatusInfo.map { mobileModel -> - // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] - if (mobileModel.isGsm) { - mobileModel.primaryLevel - } else { - mobileModel.cdmaLevel } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) /** * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] * once it's wired up inside of [CarrierConfigTracker] */ - override val numberOfLevels: Flow<Int> = flowOf(4) + override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4) - /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */ - // TODO: find a better name for this? - override val cutOut: Flow<Boolean> = flowOf(false) + override val isDataConnected: StateFlow<Boolean> = + mobileStatusInfo + .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 614d583c3c48..a4175c3a6ab1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton @@ -35,7 +36,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn /** @@ -51,12 +54,16 @@ import kotlinx.coroutines.flow.stateIn interface MobileIconsInteractor { /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow<List<SubscriptionInfo>> + /** True if the active mobile data subscription has data enabled */ + val activeDataConnectionHasDataEnabled: StateFlow<Boolean> /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ - val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ - val defaultMobileIconGroup: Flow<MobileIconGroup> + val defaultMobileIconGroup: StateFlow<MobileIconGroup> + /** True only if the default network is mobile, and validation also failed */ + val isDefaultConnectionFailed: StateFlow<Boolean> /** True once the user has been set up */ - val isUserSetup: Flow<Boolean> + val isUserSetup: StateFlow<Boolean> /** * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given * subId. Will throw if the ID is invalid @@ -79,6 +86,22 @@ constructor( private val activeMobileDataSubscriptionId = mobileConnectionsRepo.activeMobileDataSubscriptionId + private val activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> = + activeMobileDataSubscriptionId + .mapLatest { activeId -> + if (activeId == INVALID_SUBSCRIPTION_ID) { + null + } else { + mobileConnectionsRepo.getRepoForSubId(activeId) + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = + activeMobileDataConnectionRepo + .flatMapLatest { it?.dataEnabled ?: flowOf(false) } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = mobileConnectionsRepo.subscriptionsFlow @@ -132,22 +155,40 @@ constructor( */ override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.mapIconSets(it) } + .mapLatest { mobileMappingsProxy.mapIconSets(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.getDefaultIcons(it) } + .mapLatest { mobileMappingsProxy.getDefaultIcons(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) - override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + /** + * We want to show an error state when cellular has actually failed to validate, but not if some + * other transport type is active, because then we expect there not to be validation. + */ + override val isDefaultConnectionFailed: StateFlow<Boolean> = + mobileConnectionsRepo.defaultMobileNetworkConnectivity + .mapLatest { connectivityModel -> + if (!connectivityModel.isConnected) { + false + } else { + !connectivityModel.isValidated + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow /** Vends out new [MobileIconInteractor] for a particular subId */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = MobileIconInteractorImpl( + scope, + activeDataConnectionHasDataEnabled, defaultMobileIconMapping, defaultMobileIconGroup, + isDefaultConnectionFailed, mobileMappingsProxy, mobileConnectionsRepo.getRepoForSubId(subId), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 81317398f086..7869021c0501 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -24,10 +24,12 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -39,29 +41,38 @@ import kotlinx.coroutines.flow.flowOf * * TODO: figure out where carrier merged and VCN models go (probably here?) */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel constructor( val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, ) { + /** Whether or not to show the error state of [SignalDrawable] */ + private val showExclamationMark: Flow<Boolean> = + iconInteractor.isDefaultDataEnabled.mapLatest { !it } + /** An int consumable by [SignalDrawable] for display */ - var iconId: Flow<Int> = - combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + val iconId: Flow<Int> = + combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, - cutOut -> - SignalDrawable.getState(level, numberOfLevels, cutOut) + showExclamationMark -> + SignalDrawable.getState(level, numberOfLevels, showExclamationMark) } .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - var networkTypeIcon: Flow<Icon?> = - combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { - networkTypeIconGroup, - isDataEnabled -> - if (!isDataEnabled) { + val networkTypeIcon: Flow<Icon?> = + combine( + iconInteractor.networkTypeIconGroup, + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed, + ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection -> + if (!dataConnected || !dataEnabled || failedConnection) { null } else { val desc = @@ -72,5 +83,5 @@ constructor( } } - var tint: Flow<Int> = flowOf(Color.CYAN) + val tint: Flow<Int> = flowOf(Color.CYAN) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index bc2ae64dd946..e3266115f777 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -67,7 +67,7 @@ public class DeviceControlsControllerImpl @Inject constructor( internal const val QS_DEFAULT_POSITION = 7 internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" - internal const val PREFS_CONTROLS_FILE = "controls_prefs" + const val PREFS_CONTROLS_FILE = "controls_prefs" internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" private const val SEEDING_MAX = 2 } diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 10a09dd169e8..61eadeb1764f 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -44,6 +44,7 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.screenshot.ReferenceScreenshotModule; +import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeControllerImpl; @@ -89,6 +90,7 @@ import dagger.multibindings.IntoSet; includes = { AospPolicyModule.class, GestureModule.class, + MultiUserUtilsModule.class, PowerModule.class, QSModule.class, ReferenceScreenshotModule.class, diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index f9bec65ab677..52f8ef8b7ebc 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -55,6 +55,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.SidefpsController; +import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.log.SessionTracker; @@ -143,6 +144,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { private SidefpsController mSidefpsController; @Mock private KeyguardPasswordViewController mKeyguardPasswordViewControllerMock; + @Mock + private FalsingA11yDelegate mFalsingA11yDelegate; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; @@ -186,7 +189,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardStateController, mKeyguardSecurityViewFlipperController, mConfigurationController, mFalsingCollector, mFalsingManager, mUserSwitcherController, mFeatureFlags, mGlobalSettings, - mSessionTracker, Optional.of(mSidefpsController)).create(mSecurityCallback); + mSessionTracker, Optional.of(mSidefpsController), mFalsingA11yDelegate).create( + mSecurityCallback); } @Test @@ -225,7 +229,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.updateResources(); verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); // Update rotation. Should trigger update mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; @@ -233,7 +238,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.updateResources(); verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); } private void touchDown() { @@ -269,7 +275,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern); verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); } @Test @@ -282,7 +289,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern); verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager), eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); } @Test @@ -293,7 +301,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password); verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); } @Test @@ -307,7 +316,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password); verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class), any(UserSwitcherController.class), - captor.capture()); + captor.capture(), + eq(mFalsingA11yDelegate)); captor.getValue().showUnlockToContinueMessage(); verify(mKeyguardPasswordViewControllerMock).showMessage( getContext().getString(R.string.keyguard_unlock_to_continue), null); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 82d3ca785161..1bd14e558fa0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -31,6 +31,7 @@ import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT; import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED; +import static com.android.keyguard.KeyguardSecurityContainer.MODE_USER_SWITCHER; import static com.google.common.truth.Truth.assertThat; @@ -54,6 +55,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.data.source.UserRecord; @@ -87,6 +89,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { private FalsingManager mFalsingManager; @Mock private UserSwitcherController mUserSwitcherController; + @Mock + private FalsingA11yDelegate mFalsingA11yDelegate; private KeyguardSecurityContainer mKeyguardSecurityContainer; @@ -111,15 +115,14 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { when(mUserSwitcherController.getCurrentUserName()).thenReturn("Test User"); when(mUserSwitcherController.isKeyguardShowing()).thenReturn(true); } + @Test public void testOnApplyWindowInsets() { int paddingBottom = getContext().getResources() .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin); int imeInsetAmount = paddingBottom + 1; int systemBarInsetAmount = 0; - - mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController, () -> {}); + initMode(MODE_DEFAULT); Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount); Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount); @@ -140,8 +143,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin); int systemBarInsetAmount = paddingBottom + 1; - mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController, () -> {}); + initMode(MODE_DEFAULT); Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount); Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount); @@ -157,11 +159,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { @Test public void testDefaultViewMode() { - mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager, - mUserSwitcherController, () -> { - }); - mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController, () -> {}); + initMode(MODE_ONE_HANDED); + initMode(MODE_DEFAULT); ConstraintSet.Constraint viewFlipperConstraint = getViewConstraint(mSecurityViewFlipper.getId()); assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); @@ -377,8 +376,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { private void setupUserSwitcher() { when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT); - mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER, - mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> {}); + initMode(MODE_USER_SWITCHER); } private ArrayList<UserRecord> buildUserRecords(int count) { @@ -396,8 +394,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { private void setupForUpdateKeyguardPosition(boolean oneHandedMode) { int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT; - mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager, - mUserSwitcherController, () -> {}); + initMode(mode); } /** Get the ConstraintLayout constraint of the view. */ @@ -406,4 +403,10 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { constraintSet.clone(mKeyguardSecurityContainer); return constraintSet.getConstraint(viewId); } + + private void initMode(int mode) { + mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager, + mUserSwitcherController, () -> { + }, mFalsingA11yDelegate); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index 94813497cb4c..b811aab6d35f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -96,7 +96,6 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } - @Test public void testA11yDisablesTap() { assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); @@ -159,4 +158,11 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { }); assertThat(mBrightLineFalsingManager.isProximityNear()).isFalse(); } + + @Test + public void testA11yAction() { + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); + when(mFalsingDataProvider.isA11yAction()).thenReturn(true); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt new file mode 100644 index 000000000000..2c904e7e3735 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt @@ -0,0 +1,57 @@ +/* + * 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.systemui.classifier + +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK +import android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class FalsingA11yDelegateTest : SysuiTestCase() { + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var view: View + lateinit var underTest: FalsingA11yDelegate + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = FalsingA11yDelegate(falsingCollector) + } + + @Test + fun testPerformAccessibilityAction_ACTION_CLICK() { + underTest.performAccessibilityAction(view, ACTION_CLICK, null) + verify(falsingCollector).onA11yAction() + } + + @Test + fun testPerformAccessibilityAction_not_ACTION_CLICK() { + underTest.performAccessibilityAction(view, ACTION_LONG_CLICK, null) + verify(falsingCollector, never()).onA11yAction() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index fa9c41a3cbb6..442bf918ad8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -267,4 +267,10 @@ public class FalsingCollectorImplTest extends SysuiTestCase { mFalsingCollector.onTouchEvent(up); verify(mFalsingDataProvider, times(2)).onMotionEvent(any(MotionEvent.class)); } + + @Test + public void testOnA11yAction() { + mFalsingCollector.onA11yAction(); + verify(mFalsingDataProvider).onA11yAction(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index 5dc607fd342b..d315c2da0703 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -310,4 +310,10 @@ public class FalsingDataProviderTest extends ClassifierTest { // an empty array. assertThat(mDataProvider.getPriorMotionEvents()).isNotNull(); } + + @Test + public void test_MotionEventComplete_A11yAction() { + mDataProvider.onA11yAction(); + assertThat(mDataProvider.isA11yAction()).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt new file mode 100644 index 000000000000..49c7442b9708 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -0,0 +1,155 @@ +/* + * 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.systemui.controls.ui + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsMetricsLogger +import com.android.systemui.controls.CustomIconCache +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import dagger.Lazy +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ControlsUiControllerImplTest : SysuiTestCase() { + @Mock lateinit var controlsController: ControlsController + @Mock lateinit var controlsListingController: ControlsListingController + @Mock lateinit var controlActionCoordinator: ControlActionCoordinator + @Mock lateinit var activityStarter: ActivityStarter + @Mock lateinit var shadeController: ShadeController + @Mock lateinit var iconCache: CustomIconCache + @Mock lateinit var controlsMetricsLogger: ControlsMetricsLogger + @Mock lateinit var keyguardStateController: KeyguardStateController + @Mock lateinit var userFileManager: UserFileManager + @Mock lateinit var userTracker: UserTracker + val sharedPreferences = FakeSharedPreferences() + + var uiExecutor = FakeExecutor(FakeSystemClock()) + var bgExecutor = FakeExecutor(FakeSystemClock()) + lateinit var underTest: ControlsUiControllerImpl + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + ControlsUiControllerImpl( + Lazy { controlsController }, + context, + uiExecutor, + bgExecutor, + Lazy { controlsListingController }, + controlActionCoordinator, + activityStarter, + shadeController, + iconCache, + controlsMetricsLogger, + keyguardStateController, + userFileManager, + userTracker + ) + `when`( + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + 0, + 0 + ) + ) + .thenReturn(sharedPreferences) + `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) + .thenReturn(sharedPreferences) + `when`(userTracker.userId).thenReturn(0) + } + + @Test + fun testGetPreferredStructure() { + val structureInfo = mock(StructureInfo::class.java) + underTest.getPreferredStructure(listOf(structureInfo)) + verify(userFileManager, times(2)) + .getSharedPreferences( + fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + mode = 0, + userId = 0 + ) + } + + @Test + fun testGetPreferredStructure_differentUserId() { + val structureInfo = + listOf( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()), + StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()), + ) + sharedPreferences + .edit() + .putString("controls_component", structureInfo[0].componentName.flattenToString()) + .putString("controls_structure", structureInfo[0].structure.toString()) + .commit() + + val differentSharedPreferences = FakeSharedPreferences() + differentSharedPreferences + .edit() + .putString("controls_component", structureInfo[1].componentName.flattenToString()) + .putString("controls_structure", structureInfo[1].structure.toString()) + .commit() + + val previousPreferredStructure = underTest.getPreferredStructure(structureInfo) + + `when`( + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + 0, + 1 + ) + ) + .thenReturn(differentSharedPreferences) + `when`(userTracker.userId).thenReturn(1) + + val currentPreferredStructure = underTest.getPreferredStructure(structureInfo) + + assertThat(previousPreferredStructure).isEqualTo(structureInfo[0]) + assertThat(currentPreferredStructure).isEqualTo(structureInfo[1]) + assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java index 99a17a613041..9115ab3bacca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java @@ -24,6 +24,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; import android.view.View; +import android.widget.TextView; import androidx.test.filters.SmallTest; @@ -48,6 +49,7 @@ public class QSCarrierTest extends SysuiTestCase { public void setUp() throws Exception { mTestableLooper = TestableLooper.get(this); LayoutInflater inflater = LayoutInflater.from(mContext); + mContext.ensureTestableResources(); mTestableLooper.runWithLooper(() -> mQSCarrier = (QSCarrier) inflater.inflate(R.layout.qs_carrier, null)); @@ -119,4 +121,30 @@ public class QSCarrierTest extends SysuiTestCase { mQSCarrier.updateState(c, true); assertEquals(View.GONE, mQSCarrier.getRSSIView().getVisibility()); } + + @Test + public void testCarrierNameMaxWidth_smallScreen_fromResource() { + int maxEms = 10; + mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms); + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_use_large_screen_shade_header, false); + TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text); + + mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration()); + + assertEquals(maxEms, carrierText.getMaxEms()); + } + + @Test + public void testCarrierNameMaxWidth_largeScreen_maxInt() { + int maxEms = 10; + mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms); + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_use_large_screen_shade_header, true); + TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text); + + mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration()); + + assertEquals(Integer.MAX_VALUE, carrierText.getMaxEms()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 93a1243a9010..45b4353d0ec0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -129,7 +129,6 @@ import com.android.systemui.statusbar.QsFrameTranslateController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; @@ -153,7 +152,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController; import com.android.systemui.statusbar.phone.LockscreenGestureLogger; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -199,7 +197,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardBottomAreaView mKeyguardBottomArea; @Mock private KeyguardBottomAreaViewController mKeyguardBottomAreaViewController; @Mock private KeyguardBottomAreaView mQsFrame; - @Mock private NotificationIconAreaController mNotificationAreaController; @Mock private HeadsUpManagerPhone mHeadsUpManager; @Mock private NotificationShelfController mNotificationShelfController; @Mock private KeyguardStatusBarView mKeyguardStatusBar; @@ -227,7 +224,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private Resources mResources; @Mock private Configuration mConfiguration; @Mock private KeyguardClockSwitch mKeyguardClockSwitch; - @Mock private MediaHierarchyManager mMediaHiearchyManager; + @Mock private MediaHierarchyManager mMediaHierarchyManager; @Mock private ConversationNotificationManager mConversationNotificationManager; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; @@ -254,7 +251,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; @Mock private LockIconViewController mLockIconViewController; @Mock private KeyguardMediaController mKeyguardMediaController; - @Mock private PrivacyDotViewController mPrivacyDotViewController; @Mock private NavigationModeController mNavigationModeController; @Mock private NavigationBarController mNavigationBarController; @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; @@ -294,7 +290,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { private ConfigurationController mConfigurationController; private SysuiStatusBarStateController mStatusBarStateController; private NotificationPanelViewController mNotificationPanelViewController; - private View.AccessibilityDelegate mAccessibiltyDelegate; + private View.AccessibilityDelegate mAccessibilityDelegate; private NotificationsQuickSettingsContainer mNotificationContainerParent; private List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners; private Handler mMainHandler; @@ -456,7 +452,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mShadeLog, mConfigurationController, () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, - mConversationNotificationManager, mMediaHiearchyManager, + mConversationNotificationManager, mMediaHierarchyManager, mStatusBarKeyguardViewManager, mNotificationsQSContainerController, mNotificationStackScrollLayoutController, @@ -465,7 +461,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mKeyguardUserSwitcherComponentFactory, mKeyguardStatusBarViewComponentFactory, mLockscreenShadeTransitionController, - mNotificationAreaController, mAuthController, mScrimController, mUserManager, @@ -474,7 +469,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mAmbientState, mLockIconViewController, mKeyguardMediaController, - mPrivacyDotViewController, mTapAgainViewController, mNavigationModeController, mNavigationBarController, @@ -516,9 +510,9 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { ArgumentCaptor<View.AccessibilityDelegate> accessibilityDelegateArgumentCaptor = ArgumentCaptor.forClass(View.AccessibilityDelegate.class); verify(mView).setAccessibilityDelegate(accessibilityDelegateArgumentCaptor.capture()); - mAccessibiltyDelegate = accessibilityDelegateArgumentCaptor.getValue(); + mAccessibilityDelegate = accessibilityDelegateArgumentCaptor.getValue(); mNotificationPanelViewController.getStatusBarStateController() - .addCallback(mNotificationPanelViewController.mStatusBarStateListener); + .addCallback(mNotificationPanelViewController.getStatusBarStateListener()); mNotificationPanelViewController .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class)); verify(mNotificationStackScrollLayoutController) @@ -773,8 +767,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, 0 /* metaState */)); - assertThat(mNotificationPanelViewController.getClosing()).isTrue(); - assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + assertThat(mNotificationPanelViewController.isClosing()).isTrue(); + assertThat(mNotificationPanelViewController.isFlinging()).isTrue(); // simulate touch that does not exceed touch slop onTouchEvent(MotionEvent.obtain(2L /* downTime */, @@ -788,8 +782,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { 0 /* metaState */)); // fling should still be called after a touch that does not exceed touch slop - assertThat(mNotificationPanelViewController.getClosing()).isTrue(); - assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + assertThat(mNotificationPanelViewController.isClosing()).isTrue(); + assertThat(mNotificationPanelViewController.isFlinging()).isTrue(); } @Test @@ -844,7 +838,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testA11y_initializeNode() { AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); - mAccessibiltyDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo); + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo); List<AccessibilityNodeInfo.AccessibilityAction> actionList = nodeInfo.getActionList(); assertThat(actionList).containsAtLeastElementsIn( @@ -856,7 +850,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testA11y_scrollForward() { - mAccessibiltyDelegate.performAccessibilityAction( + mAccessibilityDelegate.performAccessibilityAction( mView, AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId(), null); @@ -866,7 +860,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testA11y_scrollUp() { - mAccessibiltyDelegate.performAccessibilityAction( + mAccessibilityDelegate.performAccessibilityAction( mView, AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId(), null); @@ -1329,11 +1323,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() { enableSplitShade(/* enabled= */ true); mShadeExpansionStateManager.updateState(STATE_CLOSED); - assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse(); mShadeExpansionStateManager.updateState(STATE_OPENING); - assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue(); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isTrue(); } @Test @@ -1345,18 +1339,18 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { // going to lockscreen would trigger STATE_OPENING mShadeExpansionStateManager.updateState(STATE_OPENING); - assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse(); } @Test public void testQsImmediateResetsWhenPanelOpensOrCloses() { - mNotificationPanelViewController.mQsExpandImmediate = true; + mNotificationPanelViewController.setQsExpandImmediate(true); mShadeExpansionStateManager.updateState(STATE_OPEN); - assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse(); - mNotificationPanelViewController.mQsExpandImmediate = true; + mNotificationPanelViewController.setQsExpandImmediate(true); mShadeExpansionStateManager.updateState(STATE_CLOSED); - assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse(); } @Test @@ -1399,7 +1393,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() { - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); when(mQsFrame.getX()).thenReturn(0f); when(mQsFrame.getWidth()).thenReturn(1000); when(mQsHeader.getTop()).thenReturn(0); @@ -1419,7 +1413,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking() { enableSplitShade(true); - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); when(mQsFrame.getX()).thenReturn(0f); when(mQsFrame.getWidth()).thenReturn(1000); when(mQsHeader.getTop()).thenReturn(0); @@ -1495,7 +1489,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() { - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); setIsFullWidth(true); @@ -1504,7 +1498,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() { - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); setIsFullWidth(false); @@ -1513,7 +1507,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onLayoutChange_qsNotSet_doesNotCrash() { - mNotificationPanelViewController.mQs = null; + mNotificationPanelViewController.setQs(null); triggerLayoutChange(); } @@ -1539,7 +1533,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness() { float squishinessFraction = 0.456f; - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction()) .thenReturn(squishinessFraction); when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction()) @@ -1567,7 +1561,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { public void setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness() { float lsSquishinessFraction = 0.456f; float nsslSquishinessFraction = 0.987f; - mNotificationPanelViewController.mQs = mQs; + mNotificationPanelViewController.setQs(mQs); when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction()) .thenReturn(lsSquishinessFraction); when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction()) @@ -1586,7 +1580,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() { StatusBarStateController.StateListener statusBarStateListener = - mNotificationPanelViewController.mStatusBarStateListener; + mNotificationPanelViewController.getStatusBarStateListener(); statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); @@ -1601,7 +1595,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() { StatusBarStateController.StateListener statusBarStateListener = - mNotificationPanelViewController.mStatusBarStateListener; + mNotificationPanelViewController.getStatusBarStateListener(); statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(false); @@ -1616,7 +1610,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() { StatusBarStateController.StateListener statusBarStateListener = - mNotificationPanelViewController.mStatusBarStateListener; + mNotificationPanelViewController.getStatusBarStateListener(); statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(true); @@ -1631,7 +1625,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() { StatusBarStateController.StateListener statusBarStateListener = - mNotificationPanelViewController.mStatusBarStateListener; + mNotificationPanelViewController.getStatusBarStateListener(); statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(true, false); @@ -1645,7 +1639,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() { StatusBarStateController.StateListener statusBarStateListener = - mNotificationPanelViewController.mStatusBarStateListener; + mNotificationPanelViewController.getStatusBarStateListener(); statusBarStateListener.onStateChanged(SHADE_LOCKED); mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); @@ -1664,11 +1658,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() { // Given: Shade is expanded mNotificationPanelViewController.notifyExpandingFinished(); - mNotificationPanelViewController.setIsClosing(false); + mNotificationPanelViewController.setClosing(false); // When: Shade flings to close not canceled mNotificationPanelViewController.notifyExpandingStarted(); - mNotificationPanelViewController.setIsClosing(true); + mNotificationPanelViewController.setClosing(true); mNotificationPanelViewController.onFlingEnd(false); // Then: AmbientState's mIsClosing should be set to false diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 3ff7639e9262..f96c39f007dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -406,6 +406,10 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupSummary) verify(mHeadsUpManager).showNotification(mGroupSibling1) + + // In addition make sure we have explicitly marked the summary as having interrupted due + // to the alert being transferred + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -424,6 +428,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupSummary) verify(mHeadsUpManager).showNotification(mGroupChild1) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -449,6 +454,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupSummary) verify(mHeadsUpManager).showNotification(mGroupSibling1) verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -474,6 +480,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupSummary) verify(mHeadsUpManager).showNotification(mGroupChild1) verify(mHeadsUpManager, never()).showNotification(mGroupChild2) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -512,6 +519,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mGroupPriority) verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -548,6 +556,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mGroupPriority) verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -582,6 +591,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mGroupPriority) verify(mHeadsUpManager, never()).showNotification(mGroupSibling1) verify(mHeadsUpManager, never()).showNotification(mGroupSibling2) + assertTrue(mGroupSummary.hasInterrupted()) } @Test @@ -672,6 +682,35 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + fun testNoTransfer_groupSummaryNotAlerting() { + // When we have a group where the summary should not alert and exactly one child should + // alert, we should never mark the group summary as interrupted (because it doesn't). + setShouldHeadsUp(mGroupSummary, false) + setShouldHeadsUp(mGroupChild1, true) + setShouldHeadsUp(mGroupChild2, false) + + mCollectionListener.onEntryAdded(mGroupSummary) + mCollectionListener.onEntryAdded(mGroupChild1) + mCollectionListener.onEntryAdded(mGroupChild2) + val groupEntry = GroupEntryBuilder() + .setSummary(mGroupSummary) + .setChildren(listOf(mGroupChild1, mGroupChild2)) + .build() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupEntry)) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any()) + finishBind(mGroupChild1) + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild2), any()) + + verify(mHeadsUpManager, never()).showNotification(mGroupSummary) + verify(mHeadsUpManager).showNotification(mGroupChild1) + verify(mHeadsUpManager, never()).showNotification(mGroupChild2) + assertFalse(mGroupSummary.hasInterrupted()) + } + + @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should // and is new enough to do so diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index de1fec85360b..288f54c7d03c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -17,16 +17,18 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileConnectionRepository : MobileConnectionRepository { private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) - override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow + override val subscriptionModelFlow = _subscriptionsModelFlow private val _dataEnabled = MutableStateFlow(true) override val dataEnabled = _dataEnabled + private val _isDefaultDataSubscription = MutableStateFlow(true) + override val isDefaultDataSubscription = _isDefaultDataSubscription + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { _subscriptionsModelFlow.value = model } @@ -34,4 +36,8 @@ class FakeMobileConnectionRepository : MobileConnectionRepository { fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } + + fun setIsDefaultDataSubscription(isDefault: Boolean) { + _isDefaultDataSubscription.value = isDefault + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 813e750684a0..533d5d9d5b4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -17,8 +17,9 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,18 +27,26 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow - private val _activeMobileDataSubscriptionId = - MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId private val _defaultDataSubRatConfig = MutableStateFlow(Config()) override val defaultDataSubRatConfig = _defaultDataSubRatConfig + private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + override val defaultDataSubId = _defaultDataSubId + + private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) + override val defaultMobileNetworkConnectivity = _mobileConnectivity + private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } + private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) + override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent + fun setSubscriptions(subs: List<SubscriptionInfo>) { _subscriptionsFlow.value = subs } @@ -46,6 +55,18 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { _defaultDataSubRatConfig.value = config } + fun setDefaultDataSubId(id: Int) { + _defaultDataSubId.value = id + } + + fun setMobileConnectivity(model: MobileConnectivityModel) { + _mobileConnectivity.value = model + } + + suspend fun triggerGlobalMobileDataSettingChangedEvent() { + _globalMobileDataSettingChangedEvent.emit(Unit) + } + fun setActiveMobileDataSubscriptionId(subId: Int) { _activeMobileDataSubscriptionId.value = subId } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt index 6c495c5c705a..141b50c017e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt @@ -16,13 +16,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow /** Defaults to `true` */ class FakeUserSetupRepository : UserSetupRepository { private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isUserSetupFlow: Flow<Boolean> = _isUserSetup + override val isUserSetupFlow = _isUserSetup fun setUserSetup(setup: Boolean) { _isUserSetup.value = setup diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 093936444789..5ce51bb62c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.os.UserHandle +import android.provider.Settings import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength @@ -42,6 +44,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -67,16 +70,23 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() + private val connectionsRepo = FakeMobileConnectionsRepository() @Before fun setUp() { MockitoAnnotations.initMocks(this) + globalSettings.userId = UserHandle.USER_ALL whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = MobileConnectionRepositoryImpl( + context, SUB_1_ID, telephonyManager, + globalSettings, + connectionsRepo.defaultDataSubId, + connectionsRepo.globalMobileDataSettingChangedEvent, IMMEDIATE, logger, scope, @@ -290,14 +300,20 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun dataEnabled_isEnabled() = + fun dataEnabled_initial_false() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) - var latest: Boolean? = null - val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + assertThat(underTest.dataEnabled.value).isFalse() + } - assertThat(latest).isTrue() + @Test + fun dataEnabled_isEnabled_true() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + val job = underTest.dataEnabled.launchIn(this) + + assertThat(underTest.dataEnabled.value).isTrue() job.cancel() } @@ -306,10 +322,59 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun dataEnabled_isDisabled() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + val job = underTest.dataEnabled.launchIn(this) + + assertThat(underTest.dataEnabled.value).isFalse() + + job.cancel() + } + + @Test + fun isDefaultDataSubscription_isDefault() = + runBlocking(IMMEDIATE) { + connectionsRepo.setDefaultDataSubId(SUB_1_ID) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isDefaultDataSubscription_isNotDefault() = + runBlocking(IMMEDIATE) { + // Our subId is SUB_1_ID + connectionsRepo.setDefaultDataSubId(123) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() = + runBlocking(IMMEDIATE) { + val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID" var latest: Boolean? = null val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + // We don't read the setting directly, we query telephony when changes happen + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) + assertThat(latest).isFalse() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + globalSettings.putInt(subIdSettingName, 1) + assertThat(latest).isTrue() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) assertThat(latest).isFalse() job.cancel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt index 326e0d28166f..a953a3d802e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -16,26 +16,33 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.provider.Settings import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.test.filters.SmallTest +import com.android.internal.telephony.PhoneConstants import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking @@ -43,7 +50,6 @@ import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -54,32 +60,26 @@ import org.mockito.MockitoAnnotations class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl + @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever( - broadcastDispatcher.broadcastFlow( - any(), - nullable(), - ArgumentMatchers.anyInt(), - nullable(), - ) - ) - .thenReturn(flowOf(Unit)) underTest = MobileConnectionsRepositoryImpl( + connectivityManager, subscriptionManager, telephonyManager, logger, - broadcastDispatcher, + fakeBroadcastDispatcher, + globalSettings, context, IMMEDIATE, scope, @@ -214,6 +214,139 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testDefaultDataSubId_updatesOnBroadcast() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_2_ID) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_1_ID) + + job.cancel() + } + + @Test + fun mobileConnectivity_default() { + assertThat(underTest.defaultMobileNetworkConnectivity.value) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + } + + @Test + fun mobileConnectivity_isConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true)) + + job.cancel() + } + + @Test + fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() = + runBlocking(IMMEDIATE) { + var produced = false + val job = + underTest.globalMobileDataSettingChangedEvent + .onEach { produced = true } + .launchIn(this) + + assertThat(produced).isFalse() + + globalSettings.putInt(Settings.Global.MOBILE_DATA, 0) + + assertThat(produced).isTrue() + + job.cancel() + } + + @Test + fun mobileConnectivity_isConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false)) + + job.cancel() + } + + @Test + fun mobileConnectivity_isNotConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + + job.cancel() + } + + /** In practice, I don't think this state can ever happen (!connected, validated) */ + @Test + fun mobileConnectivity_isNotConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isEqualTo(MobileConnectivityModel(false, true)) + + job.cancel() + } + + private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected) + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated) + } + + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() verify(subscriptionManager) @@ -242,5 +375,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val SUB_2_ID = 2 private val SUB_2 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + + private const val NET_ID = 123 + private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 5611c448c550..3ae7d3ca1c19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -28,18 +28,23 @@ class FakeMobileIconInteractor : MobileIconInteractor { private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly + private val _isFailedConnection = MutableStateFlow(false) + override val isDefaultConnectionFailed = _isFailedConnection + + override val isDataConnected = MutableStateFlow(true) + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled + private val _isDefaultDataEnabled = MutableStateFlow(true) + override val isDefaultDataEnabled = _isDefaultDataEnabled + private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) override val level = _level private val _numberOfLevels = MutableStateFlow(4) override val numberOfLevels = _numberOfLevels - private val _cutOut = MutableStateFlow(false) - override val cutOut = _cutOut - fun setIconGroup(group: SignalIcon.MobileIconGroup) { _iconGroup.value = group } @@ -52,6 +57,14 @@ class FakeMobileIconInteractor : MobileIconInteractor { _isDataEnabled.value = enabled } + fun setIsDefaultDataEnabled(disabled: Boolean) { + _isDefaultDataEnabled.value = disabled + } + + fun setIsFailedConnection(failed: Boolean) { + _isFailedConnection.value = failed + } + fun setLevel(level: Int) { _level.value = level } @@ -59,8 +72,4 @@ class FakeMobileIconInteractor : MobileIconInteractor { fun setNumberOfLevels(num: Int) { _numberOfLevels.value = num } - - fun setCutOut(cutOut: Boolean) { - _cutOut.value = cutOut - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 2bd228603cb0..061c3b54650e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,8 +26,7 @@ import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : - MobileIconsInteractor { +class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor { val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) val LTE_KEY = mobileMappings.toIconKey(LTE) val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) @@ -46,9 +45,14 @@ class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, ) + override val isDefaultConnectionFailed = MutableStateFlow(false) + private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val filteredSubscriptions = _filteredSubscriptions + private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) + override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index ff44af4c9204..7fc1c0f6272c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType @@ -34,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsPro import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -49,12 +51,17 @@ class MobileIconInteractorTest : SysuiTestCase() { private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) private val connectionRepository = FakeMobileConnectionRepository() + private val scope = CoroutineScope(IMMEDIATE) + @Before fun setUp() { underTest = MobileIconInteractorImpl( + scope, + mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, + mobileIconsInteractor.isDefaultConnectionFailed, mobileMappingsProxy, connectionRepository, ) @@ -196,6 +203,66 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun test_isDefaultDataEnabled_matchesParent() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this) + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true + assertThat(latest).isTrue() + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun test_isDefaultConnectionFailed_matchedParent() = + runBlocking(IMMEDIATE) { + val job = underTest.isDefaultConnectionFailed.launchIn(this) + + mobileIconsInteractor.isDefaultConnectionFailed.value = false + assertThat(underTest.isDefaultConnectionFailed.value).isFalse() + + mobileIconsInteractor.isDefaultConnectionFailed.value = true + assertThat(underTest.isDefaultConnectionFailed.value).isTrue() + + job.cancel() + } + + @Test + fun dataState_connected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected) + ) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun dataState_notConnected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected) + ) + + assertThat(latest).isFalse() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 877ce0e6b351..b56dcd752557 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -17,8 +17,10 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository @@ -32,6 +34,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.After import org.junit.Before import org.junit.Test @@ -168,6 +171,92 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun activeDataConnection_turnedOn() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun activeDataConnection_turnedOff() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + CONNECTION_1.setDataEnabled(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun activeDataConnection_invalidSubId() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID) + yield() + + // An invalid active subId should tell us that data is off + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_connected_validated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_notConnected_notValidated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_connected_notValidated_failed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false)) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index ce0f33f400ab..d4c2c3f6cc2b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -46,10 +46,12 @@ class MobileIconViewModelTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) interactor.apply { setLevel(1) - setCutOut(false) + setIsDefaultDataEnabled(true) + setIsFailedConnection(false) setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) + isDataConnected.value = true } underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) } @@ -59,8 +61,23 @@ class MobileIconViewModelTest : SysuiTestCase() { runBlocking(IMMEDIATE) { var latest: Int? = null val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal() - assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false)) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun iconId_cutout_whenDefaultDataDisabled() = + runBlocking(IMMEDIATE) { + interactor.setIsDefaultDataEnabled(false) + + var latest: Int? = null + val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal(level = 1, connected = false) + + assertThat(latest).isEqualTo(expected) job.cancel() } @@ -97,6 +114,44 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + fun networkType_nullWhenFailedConnection() = + runBlocking(IMMEDIATE) { + interactor.setIconGroup(THREE_G) + interactor.setIsDataEnabled(true) + interactor.setIsFailedConnection(true) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_nullWhenDataDisconnects() = + runBlocking(IMMEDIATE) { + val initial = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + + interactor.setIconGroup(THREE_G) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.setIconGroup(THREE_G) + assertThat(latest).isEqualTo(initial) + + interactor.isDataConnected.value = false + yield() + + assertThat(latest).isNull() + + job.cancel() + } + + @Test fun networkType_null_changeToDisabled() = runBlocking(IMMEDIATE) { val expected = @@ -119,6 +174,14 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + /** Convenience constructor for these tests */ + private fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 47b415630de8..e3ae03cbcdd8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -3651,6 +3651,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub throw new IllegalArgumentException("The display " + displayId + " does not exist or is" + " not tracked by accessibility."); } + if (mProxyManager.isProxyed(displayId)) { + throw new IllegalArgumentException("The display " + displayId + " is already being" + + "proxy-ed"); + } mProxyManager.registerProxy(client, displayId); return true; diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index 934b665d7dae..247f320adecd 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -32,6 +32,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallback; import android.view.KeyEvent; +import android.view.accessibility.AccessibilityDisplayProxy; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.Nullable; @@ -44,7 +45,7 @@ import java.util.List; import java.util.Set; /** - * Represents the system connection to an {@link android.view.accessibility.AccessibilityProxy}. + * Represents the system connection to an {@link AccessibilityDisplayProxy}. * * <p>Most methods are no-ops since this connection does not need to capture input or listen to * hardware-related changes. diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java index fb0b8f3b17b1..a2ce61063aaf 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; import android.accessibilityservice.IAccessibilityServiceClient; +import java.util.HashSet; + /** * Manages proxy connections. * @@ -26,6 +28,7 @@ import android.accessibilityservice.IAccessibilityServiceClient; */ public class ProxyManager { private final Object mLock; + private final HashSet<Integer> mDisplayIds = new HashSet<>(); ProxyManager(Object lock) { mLock = lock; @@ -35,12 +38,21 @@ public class ProxyManager { * TODO: Create the proxy service connection. */ public void registerProxy(IAccessibilityServiceClient client, int displayId) { + mDisplayIds.add(displayId); } /** * TODO: Unregister the proxy service connection based on display id. */ public boolean unregisterProxy(int displayId) { + mDisplayIds.remove(displayId); return true; } + + /** + * Checks if a display id is being proxy-ed. + */ + public boolean isProxyed(int displayId) { + return mDisplayIds.contains(displayId); + } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 47ce5928c0be..64b7688cc196 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -24,6 +24,7 @@ import static android.service.autofill.FillEventHistory.Event.UI_TYPE_MENU; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_UNKNOWN; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; +import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE; import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; @@ -416,6 +417,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private boolean mPreviouslyFillDialogPotentiallyStarted; + /** + * Keeps the fill dialog trigger ids of the last response. This invalidates + * the trigger ids of the previous response. + */ + @Nullable + @GuardedBy("mLock") + private AutofillId[] mLastFillDialogTriggerIds; + void onSwitchInputMethodLocked() { // One caveat is that for the case where the focus is on a field for which regular autofill // returns null, and augmented autofill is triggered, and then the user switches the input @@ -1222,6 +1231,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } + mLastFillDialogTriggerIds = response.getFillDialogTriggerIds(); + final int flags = response.getFlags(); if ((flags & FillResponse.FLAG_DELAY_FILL) != 0) { Slog.v(TAG, "Service requested to wait for delayed fill response."); @@ -1310,6 +1321,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // fallback to the default platform password manager mSessionFlags.mClientSuggestionsEnabled = false; + mLastFillDialogTriggerIds = null; final InlineSuggestionsRequest inlineRequest = (mLastInlineSuggestionsRequest != null @@ -1348,6 +1360,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + (timedOut ? "timeout" : "failure")); } mService.resetLastResponse(); + mLastFillDialogTriggerIds = null; final LogMaker requestLog = mRequestLogs.get(requestId); if (requestLog == null) { Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId); @@ -3049,6 +3062,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + if ((flags & FLAG_RESET_FILL_DIALOG_STATE) != 0) { + if (sDebug) Log.d(TAG, "force to reset fill dialog state"); + mSessionFlags.mFillDialogDisabled = false; + } + switch(action) { case ACTION_START_SESSION: // View is triggering autofill. @@ -3488,10 +3506,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } private boolean isFillDialogUiEnabled() { - // TODO read from Settings or somewhere - final boolean isSettingsEnabledFillDialog = true; synchronized (mLock) { - return isSettingsEnabledFillDialog && !mSessionFlags.mFillDialogDisabled; + return !mSessionFlags.mFillDialogDisabled; } } @@ -3517,14 +3533,25 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState AutofillId filledId, String filterText, int flags) { if (!isFillDialogUiEnabled()) { // Unsupported fill dialog UI + if (sDebug) Log.w(TAG, "requestShowFillDialog: fill dialog is disabled"); return false; } if ((flags & FillRequest.FLAG_IME_SHOWING) != 0) { // IME is showing, fallback to normal suggestions UI + if (sDebug) Log.w(TAG, "requestShowFillDialog: IME is showing"); return false; } + synchronized (mLock) { + if (mLastFillDialogTriggerIds == null + || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) { + // Last fill dialog triggered ids are changed. + if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed."); + return false; + } + } + final Drawable serviceIcon = getServiceIcon(); getUiForShowing().showFillDialog(filledId, response, filterText, @@ -4394,6 +4421,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (mSessionFlags.mAugmentedAutofillOnly) { pw.print(prefix); pw.println("For Augmented Autofill Only"); } + if (mSessionFlags.mFillDialogDisabled) { + pw.print(prefix); pw.println("Fill Dialog disabled"); + } + if (mLastFillDialogTriggerIds != null) { + pw.print(prefix); pw.println("Last Fill Dialog trigger ids: "); + pw.println(mSelectedDatasetIds); + } if (mAugmentedAutofillDestroyer != null) { pw.print(prefix); pw.println("has mAugmentedAutofillDestroyer"); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 2eaddb1c37f1..062afe93e63d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13874,6 +13874,29 @@ public class ActivityManagerService extends IActivityManager.Stub @Nullable IBinder backgroundActivityStartsToken, @Nullable int[] broadcastAllowList, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { + final int cookie = BroadcastQueue.traceBegin("broadcastIntentLockedTraced"); + final int res = broadcastIntentLockedTraced(callerApp, callerPackage, callerFeatureId, + intent, resolvedType, resultToApp, resultTo, resultCode, resultData, resultExtras, + requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions, + ordered, sticky, callingPid, callingUid, realCallingUid, realCallingPid, userId, + allowBackgroundActivityStarts, backgroundActivityStartsToken, broadcastAllowList, + filterExtrasForReceiver); + BroadcastQueue.traceEnd(cookie); + return res; + } + + @GuardedBy("this") + final int broadcastIntentLockedTraced(ProcessRecord callerApp, String callerPackage, + @Nullable String callerFeatureId, Intent intent, String resolvedType, + ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData, + Bundle resultExtras, String[] requiredPermissions, + String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions, + boolean ordered, boolean sticky, int callingPid, int callingUid, + int realCallingUid, int realCallingPid, int userId, + boolean allowBackgroundActivityStarts, + @Nullable IBinder backgroundActivityStartsToken, + @Nullable int[] broadcastAllowList, + @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { // Ensure all internal loopers are registered for idle checks BroadcastLoopers.addMyLooper(); @@ -14425,6 +14448,7 @@ public class ActivityManagerService extends IActivityManager.Stub } // Figure out who all will receive this broadcast. + final int cookie = BroadcastQueue.traceBegin("queryReceivers"); List receivers = null; List<BroadcastFilter> registeredReceivers = null; // Need to resolve the intent to interested receivers... @@ -14455,6 +14479,7 @@ public class ActivityManagerService extends IActivityManager.Stub resolvedType, false /*defaultOnly*/, userId); } } + BroadcastQueue.traceEnd(cookie); final boolean replacePending = (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0; diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java index bebb48473fc3..b828720c9162 100644 --- a/services/core/java/com/android/server/am/BroadcastLoopers.java +++ b/services/core/java/com/android/server/am/BroadcastLoopers.java @@ -25,6 +25,8 @@ import android.os.SystemClock; import android.util.ArraySet; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; + import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -37,6 +39,7 @@ import java.util.concurrent.CountDownLatch; public class BroadcastLoopers { private static final String TAG = "BroadcastLoopers"; + @GuardedBy("sLoopers") private static final ArraySet<Looper> sLoopers = new ArraySet<>(); /** diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index f7d24e9b8b4e..2e12309ed240 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -114,7 +114,14 @@ class BroadcastProcessQueue { * dispatched to this process, in the same representation as * {@link #mPending}. */ - private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(); + private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(4); + + /** + * Ordered collection of "offload" broadcasts that are waiting to be + * dispatched to this process, in the same representation as + * {@link #mPending}. + */ + private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4); /** * Broadcast actively being dispatched to this process. @@ -148,8 +155,7 @@ class BroadcastProcessQueue { private boolean mActiveViaColdStart; /** - * Count of {@link #mPending} and {@link #mPendingUrgent} broadcasts of - * these various flavors. + * Count of pending broadcasts of these various flavors. */ private int mCountForeground; private int mCountOrdered; @@ -177,6 +183,16 @@ class BroadcastProcessQueue { this.uid = uid; } + private @NonNull ArrayDeque<SomeArgs> getQueueForBroadcast(@NonNull BroadcastRecord record) { + if (record.isUrgent()) { + return mPendingUrgent; + } else if (record.isOffload()) { + return mPendingOffload; + } else { + return mPending; + } + } + /** * Enqueue the given broadcast to be dispatched to this process at some * future point in time. The target receiver is indicated by the given index @@ -193,10 +209,12 @@ class BroadcastProcessQueue { public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { if (record.isReplacePending()) { - boolean didReplace = replaceBroadcastInQueue(mPending, - record, recordIndex, blockedUntilTerminalCount) - || replaceBroadcastInQueue(mPendingUrgent, - record, recordIndex, blockedUntilTerminalCount); + boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex, + blockedUntilTerminalCount) + || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex, + blockedUntilTerminalCount) + || replaceBroadcastInQueue(mPendingOffload, record, recordIndex, + blockedUntilTerminalCount); if (didReplace) { return; } @@ -213,8 +231,7 @@ class BroadcastProcessQueue { // issued ahead of others that are already pending, for example if this new // broadcast is in a different delivery class or is tied to a direct user interaction // with implicit responsiveness expectations. - final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending; - queue.addLast(newBroadcastArgs); + getQueueForBroadcast(record).addLast(newBroadcastArgs); onBroadcastEnqueued(record, recordIndex); } @@ -227,7 +244,7 @@ class BroadcastProcessQueue { * {@code false} otherwise. */ private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, - @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { + @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { final Iterator<SomeArgs> it = queue.descendingIterator(); final Object receiver = record.receivers.get(recordIndex); while (it.hasNext()) { @@ -279,10 +296,13 @@ class BroadcastProcessQueue { */ public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer, boolean andRemove) { - boolean didSomething = forEachMatchingBroadcastInQueue(mPending, + boolean didSomething = false; + didSomething |= forEachMatchingBroadcastInQueue(mPending, predicate, consumer, andRemove); didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent, predicate, consumer, andRemove); + didSomething |= forEachMatchingBroadcastInQueue(mPendingOffload, + predicate, consumer, andRemove); return didSomething; } @@ -516,7 +536,7 @@ class BroadcastProcessQueue { } public boolean isEmpty() { - return mPending.isEmpty() && mPendingUrgent.isEmpty(); + return mPending.isEmpty() && mPendingUrgent.isEmpty() && mPendingOffload.isEmpty(); } public boolean isActive() { @@ -537,6 +557,8 @@ class BroadcastProcessQueue { return mPendingUrgent; } else if (!mPending.isEmpty()) { return mPending; + } else if (!mPendingOffload.isEmpty()) { + return mPendingOffload; } return null; } @@ -581,12 +603,15 @@ class BroadcastProcessQueue { } final SomeArgs next = mPending.peekFirst(); final SomeArgs nextUrgent = mPendingUrgent.peekFirst(); + final SomeArgs nextOffload = mPendingOffload.peekFirst(); // Empty queue is past any barrier - final boolean nextLater = next == null + final boolean nextLater = (next == null) || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; - final boolean nextUrgentLater = nextUrgent == null + final boolean nextUrgentLater = (nextUrgent == null) || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime; - return nextLater && nextUrgentLater; + final boolean nextOffloadLater = (nextOffload == null) + || ((BroadcastRecord) nextOffload.arg1).enqueueTime > barrierTime; + return nextLater && nextUrgentLater && nextOffloadLater; } public boolean isRunnable() { @@ -726,8 +751,9 @@ class BroadcastProcessQueue { // If we have too many broadcasts pending, bypass any delays that // might have been applied above to aid draining - if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) { - mRunnableAt = runnableAt; + if (mPending.size() + mPendingUrgent.size() + + mPendingOffload.size() >= constants.MAX_PENDING_BROADCASTS) { + mRunnableAt = Math.min(mRunnableAt, runnableAt); mRunnableAtReason = REASON_MAX_PENDING; } } else { @@ -845,23 +871,28 @@ class BroadcastProcessQueue { pw.println(); pw.increaseIndent(); if (mActive != null) { - dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount); + dumpRecord("ACTIVE", now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount); } for (SomeArgs args : mPendingUrgent) { final BroadcastRecord r = (BroadcastRecord) args.arg1; - dumpRecord(now, pw, r, args.argi1, args.argi2); + dumpRecord("URGENT", now, pw, r, args.argi1, args.argi2); } for (SomeArgs args : mPending) { final BroadcastRecord r = (BroadcastRecord) args.arg1; - dumpRecord(now, pw, r, args.argi1, args.argi2); + dumpRecord(null, now, pw, r, args.argi1, args.argi2); + } + for (SomeArgs args : mPendingOffload) { + final BroadcastRecord r = (BroadcastRecord) args.arg1; + dumpRecord("OFFLOAD", now, pw, r, args.argi1, args.argi2); } pw.decreaseIndent(); pw.println(); } @NeverCompile - private void dumpRecord(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw, - @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { + private void dumpRecord(@Nullable String flavor, @UptimeMillisLong long now, + @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record, int recordIndex, + int blockedUntilTerminalCount) { TimeUtils.formatDuration(record.enqueueTime, now, pw); pw.print(' '); pw.println(record.toShortString()); @@ -872,6 +903,10 @@ class BroadcastProcessQueue { pw.print(" at "); TimeUtils.formatDuration(record.scheduledTime[recordIndex], now, pw); } + if (flavor != null) { + pw.print(' '); + pw.print(flavor); + } final Object receiver = record.receivers.get(recordIndex); if (receiver instanceof BroadcastFilter) { final BroadcastFilter filter = (BroadcastFilter) receiver; diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 1e172fc92f40..e0fab2cfec21 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.DropBoxManager; import android.os.Handler; +import android.os.Trace; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -76,6 +77,18 @@ public abstract class BroadcastQueue { } } + static int traceBegin(@NonNull String methodName) { + final int cookie = methodName.hashCode(); + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, + TAG, methodName, cookie); + return cookie; + } + + static void traceEnd(int cookie) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, + TAG, cookie); + } + @Override public String toString() { return mQueueName; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 57506199f0e1..af2a97e62617 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -64,7 +64,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; -import android.os.Trace; import android.os.UserHandle; import android.text.format.DateUtils; import android.util.IndentingPrintWriter; @@ -144,14 +143,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mRunning = new BroadcastProcessQueue[mConstants.MAX_RUNNING_PROCESS_QUEUES]; } - // TODO: add support for replacing pending broadcasts - // TODO: add support for merging pending broadcasts - - // TODO: consider reordering foreground broadcasts within queue - - // TODO: pause queues when background services are running - // TODO: pause queues when processes are frozen - /** * Map from UID to per-process broadcast queues. If a UID hosts more than * one process, each additional process is stored as a linked list using @@ -222,12 +213,22 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private static final int MSG_DELIVERY_TIMEOUT_HARD = 3; private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4; private static final int MSG_CHECK_HEALTH = 5; + private static final int MSG_FINISH_RECEIVER = 6; private void enqueueUpdateRunningList() { mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST); mLocalHandler.sendEmptyMessage(MSG_UPDATE_RUNNING_LIST); } + private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue, + @DeliveryState int deliveryState, @NonNull String reason) { + final SomeArgs args = SomeArgs.obtain(); + args.arg1 = queue; + args.argi1 = deliveryState; + args.arg2 = reason; + mLocalHandler.sendMessage(Message.obtain(mLocalHandler, MSG_FINISH_RECEIVER, args)); + } + private final Handler mLocalHandler; private final Handler.Callback mLocalCallback = (msg) -> { @@ -266,6 +267,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } return true; } + case MSG_FINISH_RECEIVER: { + synchronized (mService) { + final SomeArgs args = (SomeArgs) msg.obj; + final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1; + final int deliveryState = args.argi1; + final String reason = (String) args.arg2; + args.recycle(); + finishReceiverLocked(queue, deliveryState, reason); + } + return true; + } } return false; }; @@ -309,6 +321,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { return; } + final int cookie = traceBegin("updateRunnableList"); final boolean wantQueue = queue.isRunnable(); final boolean inQueue = (queue == mRunnableHead) || (queue.runnableAtPrev != null) || (queue.runnableAtNext != null); @@ -335,6 +348,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (queue.isEmpty() && !queue.isActive() && !queue.isProcessWarm()) { removeProcessQueue(queue.processName, queue.uid); } + + traceEnd(cookie); } /** @@ -349,7 +364,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { int avail = mRunning.length - getRunningSize(); if (avail == 0) return; - final int cookie = traceBegin(TAG, "updateRunningList"); + final int cookie = traceBegin("updateRunningList"); final long now = SystemClock.uptimeMillis(); // If someone is waiting for a state, everything is runnable now @@ -449,7 +464,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }); } - traceEnd(TAG, cookie); + traceEnd(cookie); } @Override @@ -516,7 +531,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (queue != null) { // If queue was running a broadcast, fail it if (queue.isActive()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE, + "onApplicationCleanupLocked"); } // Skip any pending registered receivers, since the old process @@ -544,6 +560,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { public void enqueueBroadcastLocked(@NonNull BroadcastRecord r) { if (DEBUG_BROADCAST) logv("Enqueuing " + r + " for " + r.receivers.size() + " receivers"); + final int cookie = traceBegin("enqueueBroadcast"); r.applySingletonPolicy(mService); final IntentFilter removeMatchingFilter = (r.options != null) @@ -613,6 +630,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (r.receivers.isEmpty()) { scheduleResultTo(r); } + + traceEnd(cookie); } private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) { @@ -668,7 +687,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // Ignore registered receivers from a previous PID if (receiver instanceof BroadcastFilter) { mRunningColdStart = null; - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, + "BroadcastFilter for cold app"); return; } @@ -690,7 +710,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { hostingRecord, zygotePolicyFlags, allowWhileBooting, false); if (queue.app == null) { mRunningColdStart = null; - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, + "startProcessLocked failed"); return; } } @@ -721,33 +742,37 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // If someone already finished this broadcast, finish immediately final int oldDeliveryState = getDeliveryState(r, index); if (isDeliveryStateTerminal(oldDeliveryState)) { - finishReceiverLocked(queue, oldDeliveryState); + enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state"); return; } // Consider additional cases where we'd want to finish immediately if (app.isInFullBackup()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup"); return; } if (mSkipPolicy.shouldSkip(r, receiver)) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy"); return; } final Intent receiverIntent = r.getReceiverIntent(receiver); if (receiverIntent == null) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup"); return; } // Ignore registered receivers from a previous PID if ((receiver instanceof BroadcastFilter) && ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, + "BroadcastFilter for mismatched PID"); return; } - if (mService.mProcessesReady && !r.timeoutExempt) { + // Skip ANR tracking early during boot, when requested, or when we + // immediately assume delivery success + final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered; + if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) { queue.lastCpuDelayTime = queue.app.getCpuDelayTime(); final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT; @@ -775,7 +800,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); - setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED); + setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED, + "scheduleReceiverWarmLocked"); final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { @@ -789,8 +815,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // TODO: consider making registered receivers of unordered // broadcasts report results to detect ANRs - if (!r.ordered) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + if (assumeDelivered) { + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_DELIVERED, + "assuming delivered"); } } else { notifyScheduleReceiver(app, r, (ResolveInfo) receiver); @@ -804,10 +831,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { logw(msg); app.scheduleCrashLocked(msg, CannotDeliverBroadcastException.TYPE_ID, null); app.setKilled(true); - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, "remote app"); } } else { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, + "missing IApplicationThread"); } } @@ -851,7 +879,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } private void deliveryTimeoutHardLocked(@NonNull BroadcastProcessQueue queue) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT, + "deliveryTimeoutHardLocked"); } @Override @@ -878,16 +907,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (r.resultAbort) { for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { setDeliveryState(null, null, r, i, r.receivers.get(i), - BroadcastRecord.DELIVERY_SKIPPED); + BroadcastRecord.DELIVERY_SKIPPED, "resultAbort"); } } } - return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app"); } private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue, - @DeliveryState int deliveryState) { + @DeliveryState int deliveryState, @NonNull String reason) { + final int cookie = traceBegin("finishReceiver"); checkState(queue.isActive(), "isActive"); final ProcessRecord app = queue.app; @@ -895,7 +925,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final int index = queue.getActiveIndex(); final Object receiver = r.receivers.get(index); - setDeliveryState(queue, app, r, index, receiver, deliveryState); + setDeliveryState(queue, app, r, index, receiver, deliveryState, reason); if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) { r.anrCount++; @@ -914,11 +944,12 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final boolean shouldRetire = (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS); + final boolean res; if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) { // We're on a roll; move onto the next broadcast for this process queue.makeActiveNextPending(); scheduleReceiverWarmLocked(queue); - return true; + res = true; } else { // We've drained running broadcasts; maybe move back to runnable queue.makeActiveIdle(); @@ -932,8 +963,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // Tell other OS components that app is not actively running, giving // a chance to update OOM adjustment notifyStoppedRunning(queue); - return false; + res = false; } + traceEnd(cookie); + return res; } /** @@ -942,7 +975,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { */ private void setDeliveryState(@Nullable BroadcastProcessQueue queue, @Nullable ProcessRecord app, @NonNull BroadcastRecord r, int index, - @NonNull Object receiver, @DeliveryState int newDeliveryState) { + @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) { + final int cookie = traceBegin("setDeliveryState"); final int oldDeliveryState = getDeliveryState(r, index); // Only apply state when we haven't already reached a terminal state; @@ -970,7 +1004,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { logw("Delivery state of " + r + " to " + receiver + " via " + app + " changed from " + deliveryStateToString(oldDeliveryState) + " to " - + deliveryStateToString(newDeliveryState)); + + deliveryStateToString(newDeliveryState) + " because " + reason); } r.terminalCount++; @@ -1000,6 +1034,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { enqueueUpdateRunningList(); } } + + traceEnd(cookie); } private @DeliveryState int getDeliveryState(@NonNull BroadcastRecord r, int index) { @@ -1060,7 +1096,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * of it matching a predicate. */ private final BroadcastConsumer mBroadcastConsumerSkip = (r, i) -> { - setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED); + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED, + "mBroadcastConsumerSkip"); }; /** @@ -1068,7 +1105,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * cancelled, usually as a result of it matching a predicate. */ private final BroadcastConsumer mBroadcastConsumerSkipAndCanceled = (r, i) -> { - setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED); + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED, + "mBroadcastConsumerSkipAndCanceled"); r.resultCode = Activity.RESULT_CANCELED; r.resultData = null; r.resultExtras = null; @@ -1260,18 +1298,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } - private int traceBegin(String trackName, String methodName) { - final int cookie = methodName.hashCode(); - Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - trackName, methodName, cookie); - return cookie; - } - - private void traceEnd(String trackName, int cookie) { - Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, - trackName, cookie); - } - private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) { if (!queue.isProcessWarm()) { queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid)); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 2a3c8974361f..65f9b9b4b557 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -617,6 +617,10 @@ final class BroadcastRecord extends Binder { return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0; } + boolean isOffload() { + return (intent.getFlags() & Intent.FLAG_RECEIVER_OFFLOAD) != 0; + } + /** * Core policy determination about this broadcast's delivery prioritization */ diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 60fddf0c7f22..481ab17b609e 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -21,6 +21,7 @@ import static com.android.server.am.ActivityManagerService.checkComponentPermiss import static com.android.server.am.BroadcastQueue.TAG; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -42,6 +43,8 @@ import android.util.Slog; import com.android.internal.util.ArrayUtils; +import java.util.Objects; + /** * Policy logic that decides if delivery of a particular {@link BroadcastRecord} * should be skipped for a given {@link ResolveInfo} or {@link BroadcastFilter}. @@ -51,8 +54,8 @@ import com.android.internal.util.ArrayUtils; public class BroadcastSkipPolicy { private final ActivityManagerService mService; - public BroadcastSkipPolicy(ActivityManagerService service) { - mService = service; + public BroadcastSkipPolicy(@NonNull ActivityManagerService service) { + mService = Objects.requireNonNull(service); } /** @@ -60,18 +63,39 @@ public class BroadcastSkipPolicy { * the given {@link BroadcastFilter} or {@link ResolveInfo}. */ public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull Object target) { + final String msg = shouldSkipMessage(r, target); + if (msg != null) { + Slog.w(TAG, msg); + return true; + } else { + return false; + } + } + + /** + * Determine if the given {@link BroadcastRecord} is eligible to be sent to + * the given {@link BroadcastFilter} or {@link ResolveInfo}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. + */ + public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) { if (target instanceof BroadcastFilter) { - return shouldSkip(r, (BroadcastFilter) target); + return shouldSkipMessage(r, (BroadcastFilter) target); } else { - return shouldSkip(r, (ResolveInfo) target); + return shouldSkipMessage(r, (ResolveInfo) target); } } /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link ResolveInfo}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. */ - public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull ResolveInfo info) { + private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, + @NonNull ResolveInfo info) { final BroadcastOptions brOptions = r.options; final ComponentName component = new ComponentName( info.activityInfo.applicationInfo.packageName, @@ -82,58 +106,52 @@ public class BroadcastSkipPolicy { < brOptions.getMinManifestReceiverApiLevel() || info.activityInfo.applicationInfo.targetSdkVersion > brOptions.getMaxManifestReceiverApiLevel())) { - Slog.w(TAG, "Target SDK mismatch: receiver " + info.activityInfo + return "Target SDK mismatch: receiver " + info.activityInfo + " targets " + info.activityInfo.applicationInfo.targetSdkVersion + " but delivery restricted to [" + brOptions.getMinManifestReceiverApiLevel() + ", " + brOptions.getMaxManifestReceiverApiLevel() - + "] broadcasting " + broadcastDescription(r, component)); - return true; + + "] broadcasting " + broadcastDescription(r, component); } if (brOptions != null && !brOptions.testRequireCompatChange(info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Compat change filtered: broadcasting " + broadcastDescription(r, component) + return "Compat change filtered: broadcasting " + broadcastDescription(r, component) + " to uid " + info.activityInfo.applicationInfo.uid + " due to compat change " - + r.options.getRequireCompatChangeId()); - return true; + + r.options.getRequireCompatChangeId(); } if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid, component.getPackageName(), info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Association not allowed: broadcasting " - + broadcastDescription(r, component)); - return true; + return "Association not allowed: broadcasting " + + broadcastDescription(r, component); } if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid, r.callingPid, r.resolvedType, info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Firewall blocked: broadcasting " - + broadcastDescription(r, component)); - return true; + return "Firewall blocked: broadcasting " + + broadcastDescription(r, component); } int perm = checkComponentPermission(info.activityInfo.permission, r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid, info.activityInfo.exported); if (perm != PackageManager.PERMISSION_GRANTED) { if (!info.activityInfo.exported) { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + broadcastDescription(r, component) - + " is not exported from uid " + info.activityInfo.applicationInfo.uid); + + " is not exported from uid " + info.activityInfo.applicationInfo.uid; } else { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + broadcastDescription(r, component) - + " requires " + info.activityInfo.permission); + + " requires " + info.activityInfo.permission; } - return true; } else if (info.activityInfo.permission != null) { final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission); if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid, r.callerPackage, r.callerFeatureId, "Broadcast delivered to " + info.activityInfo.name) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: broadcasting " + return "Appop Denial: broadcasting " + broadcastDescription(r, component) + " requires appop " + AppOpsManager.permissionToOp( - info.activityInfo.permission)); - return true; + info.activityInfo.permission); } } @@ -142,38 +160,34 @@ public class BroadcastSkipPolicy { android.Manifest.permission.INTERACT_ACROSS_USERS, info.activityInfo.applicationInfo.uid) != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: Receiver " + component.flattenToShortString() + return "Permission Denial: Receiver " + component.flattenToShortString() + " requests FLAG_SINGLE_USER, but app does not hold " - + android.Manifest.permission.INTERACT_ACROSS_USERS); - return true; + + android.Manifest.permission.INTERACT_ACROSS_USERS; } } if (info.activityInfo.applicationInfo.isInstantApp() && r.callingUid != info.activityInfo.applicationInfo.uid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")" - + " Instant Apps do not support manifest receivers"); - return true; + + " Instant Apps do not support manifest receivers"; } if (r.callerInstantApp && (info.activityInfo.flags & ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0 && r.callingUid != info.activityInfo.applicationInfo.uid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " requires receiver have visibleToInstantApps set" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } if (r.curApp != null && r.curApp.mErrorState.isCrashing()) { // If the target process is crashing, just skip it. - Slog.w(TAG, "Skipping deliver ordered [" + r.queue.toString() + "] " + r - + " to " + r.curApp + ": process crashing"); - return true; + return "Skipping deliver ordered [" + r.queue.toString() + "] " + r + + " to " + r.curApp + ": process crashing"; } boolean isAvailable = false; @@ -183,15 +197,13 @@ public class BroadcastSkipPolicy { UserHandle.getUserId(info.activityInfo.applicationInfo.uid)); } catch (Exception e) { // all such failures mean we skip this receiver - Slog.w(TAG, "Exception getting recipient info for " - + info.activityInfo.packageName, e); + return "Exception getting recipient info for " + + info.activityInfo.packageName; } if (!isAvailable) { - Slog.w(TAG, - "Skipping delivery to " + info.activityInfo.packageName + " / " + return "Skipping delivery to " + info.activityInfo.packageName + " / " + info.activityInfo.applicationInfo.uid - + " : package no longer available"); - return true; + + " : package no longer available"; } // If permissions need a review before any of the app components can run, we drop @@ -201,10 +213,8 @@ public class BroadcastSkipPolicy { if (!requestStartTargetPermissionsReviewIfNeededLocked(r, info.activityInfo.packageName, UserHandle.getUserId( info.activityInfo.applicationInfo.uid))) { - Slog.w(TAG, - "Skipping delivery: permission review required for " - + broadcastDescription(r, component)); - return true; + return "Skipping delivery: permission review required for " + + broadcastDescription(r, component); } final int allowed = mService.getAppStartModeLOSP( @@ -216,10 +226,9 @@ public class BroadcastSkipPolicy { // to it and the app is in a state that should not receive it // (depending on how getAppStartModeLOSP has determined that). if (allowed == ActivityManager.APP_START_MODE_DISABLED) { - Slog.w(TAG, "Background execution disabled: receiving " + return "Background execution disabled: receiving " + r.intent + " to " - + component.flattenToShortString()); - return true; + + component.flattenToShortString(); } else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) || (r.intent.getComponent() == null && r.intent.getPackage() == null @@ -228,10 +237,9 @@ public class BroadcastSkipPolicy { && !isSignaturePerm(r.requiredPermissions))) { mService.addBackgroundCheckViolationLocked(r.intent.getAction(), component.getPackageName()); - Slog.w(TAG, "Background execution not allowed: receiving " + return "Background execution not allowed: receiving " + r.intent + " to " - + component.flattenToShortString()); - return true; + + component.flattenToShortString(); } } @@ -239,10 +247,8 @@ public class BroadcastSkipPolicy { && !mService.mUserController .isUserRunning(UserHandle.getUserId(info.activityInfo.applicationInfo.uid), 0 /* flags */)) { - Slog.w(TAG, - "Skipping delivery to " + info.activityInfo.packageName + " / " - + info.activityInfo.applicationInfo.uid + " : user is not running"); - return true; + return "Skipping delivery to " + info.activityInfo.packageName + " / " + + info.activityInfo.applicationInfo.uid + " : user is not running"; } if (r.excludedPermissions != null && r.excludedPermissions.length > 0) { @@ -268,13 +274,15 @@ public class BroadcastSkipPolicy { info.activityInfo.applicationInfo.uid, info.activityInfo.packageName) == AppOpsManager.MODE_ALLOWED)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to excluded permission " + excludedPermission; } } else { // When there is no app op associated with the permission, // skip when permission is granted. if (perm == PackageManager.PERMISSION_GRANTED) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to excluded permission " + excludedPermission; } } } @@ -283,13 +291,12 @@ public class BroadcastSkipPolicy { // Check that the receiver does *not* belong to any of the excluded packages if (r.excludedPackages != null && r.excludedPackages.length > 0) { if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) { - Slog.w(TAG, "Skipping delivery of excluded package " + return "Skipping delivery of excluded package " + r.intent + " to " + component.flattenToShortString() + " excludes package " + component.getPackageName() + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } @@ -307,95 +314,94 @@ public class BroadcastSkipPolicy { perm = PackageManager.PERMISSION_DENIED; } if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " requires " + requiredPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } int appOp = AppOpsManager.permissionToOpCode(requiredPermission); if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + appOp; } } } } if (r.appOp != AppOpsManager.OP_NONE) { if (!noteOpForManifestReceiver(r.appOp, r, info, component)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + r.appOp; } } - return false; + return null; } /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link BroadcastFilter}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. */ - public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull BroadcastFilter filter) { + private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, + @NonNull BroadcastFilter filter) { if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) { - Slog.w(TAG, "Compat change filtered: broadcasting " + r.intent.toString() + return "Compat change filtered: broadcasting " + r.intent.toString() + " to uid " + filter.owningUid + " due to compat change " - + r.options.getRequireCompatChangeId()); - return true; + + r.options.getRequireCompatChangeId(); } if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid, filter.packageName, filter.owningUid)) { - Slog.w(TAG, "Association not allowed: broadcasting " + return "Association not allowed: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ") to " + filter.packageName + " through " - + filter); - return true; + + filter; } if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid, r.callingPid, r.resolvedType, filter.receiverList.uid)) { - Slog.w(TAG, "Firewall blocked: broadcasting " + return "Firewall blocked: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ") to " + filter.packageName + " through " - + filter); - return true; + + filter; } // Check that the sender has permission to send to this receiver if (filter.requiredPermission != null) { int perm = checkComponentPermission(filter.requiredPermission, r.callingPid, r.callingUid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ")" + " requires " + filter.requiredPermission - + " due to registered receiver " + filter); - return true; + + " due to registered receiver " + filter; } else { final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission); if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid, r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver") != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: broadcasting " + return "Appop Denial: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ")" + " requires appop " + AppOpsManager.permissionToOp( filter.requiredPermission) - + " due to registered receiver " + filter); - return true; + + " due to registered receiver " + filter; } } } if ((filter.receiverList.app == null || filter.receiverList.app.isKilled() || filter.receiverList.app.mErrorState.isCrashing())) { - Slog.w(TAG, "Skipping deliver [" + r.queue.toString() + "] " + r - + " to " + filter.receiverList + ": process gone or crashing"); - return true; + return "Skipping deliver [" + r.queue.toString() + "] " + r + + " to " + filter.receiverList + ": process gone or crashing"; } // Ensure that broadcasts are only sent to other Instant Apps if they are marked as @@ -405,28 +411,26 @@ public class BroadcastSkipPolicy { if (!visibleToInstantApps && filter.instantApp && filter.receiverList.uid != r.callingUid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")" - + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS"); - return true; + + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS"; } if (!filter.visibleToInstantApp && r.callerInstantApp && filter.receiverList.uid != r.callingUid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires receiver be visible to instant apps" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } // Check that the receiver has the required permission(s) to receive this broadcast. @@ -436,15 +440,14 @@ public class BroadcastSkipPolicy { int perm = checkComponentPermission(requiredPermission, filter.receiverList.pid, filter.receiverList.uid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires " + requiredPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } int appOp = AppOpsManager.permissionToOpCode(requiredPermission); if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp @@ -452,7 +455,7 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName, filter.featureId, "Broadcast delivered to registered receiver " + filter.receiverId) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid @@ -460,8 +463,7 @@ public class BroadcastSkipPolicy { + " requires appop " + AppOpsManager.permissionToOp( requiredPermission) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } } @@ -469,14 +471,13 @@ public class BroadcastSkipPolicy { int perm = checkComponentPermission(null, filter.receiverList.pid, filter.receiverList.uid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: security check failed when receiving " + return "Permission Denial: security check failed when receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } // Check that the receiver does *not* have any excluded permissions @@ -496,7 +497,7 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName) == AppOpsManager.MODE_ALLOWED)) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid @@ -504,22 +505,20 @@ public class BroadcastSkipPolicy { + " excludes appop " + AppOpsManager.permissionToOp( excludedPermission) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } else { // When there is no app op associated with the permission, // skip when permission is granted. if (perm == PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " excludes " + excludedPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } } @@ -528,15 +527,14 @@ public class BroadcastSkipPolicy { // Check that the receiver does *not* belong to any of the excluded packages if (r.excludedPackages != null && r.excludedPackages.length > 0) { if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) { - Slog.w(TAG, "Skipping delivery of excluded package " + return "Skipping delivery of excluded package " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " excludes package " + filter.packageName + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } @@ -546,15 +544,14 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName, filter.featureId, "Broadcast delivered to registered receiver " + filter.receiverId) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires appop " + AppOpsManager.opToName(r.appOp) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } // Ensure that broadcasts are only sent to other apps if they are explicitly marked as @@ -562,15 +559,14 @@ public class BroadcastSkipPolicy { if (!filter.exported && checkComponentPermission(null, r.callingPid, r.callingUid, filter.receiverList.uid, filter.exported) != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Exported Denial: sending " + return "Exported Denial: sending " + r.intent.toString() + ", action: " + r.intent.getAction() + " from " + r.callerPackage + " (uid=" + r.callingUid + ")" + " due to receiver " + filter.receiverList.app + " (uid " + filter.receiverList.uid + ")" - + " not specifying RECEIVER_EXPORTED"); - return true; + + " not specifying RECEIVER_EXPORTED"; } // If permissions need a review before any of the app components can run, we drop @@ -579,10 +575,10 @@ public class BroadcastSkipPolicy { // broadcast. if (!requestStartTargetPermissionsReviewIfNeededLocked(r, filter.packageName, filter.owningUserId)) { - return true; + return "Skipping delivery to " + filter.packageName + " due to permissions review"; } - return false; + return null; } private static String broadcastDescription(BroadcastRecord r, ComponentName component) { diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 82d239f59f1e..8d3890ce9d5a 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -2185,8 +2185,6 @@ class UserController implements Handler.Callback { mHandler.sendMessage(mHandler.obtainMessage(COMPLETE_USER_SWITCH_MSG, newUserId, 0)); uss.switching = false; - mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); - mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0)); stopGuestOrEphemeralUserIfBackground(oldUserId); stopUserOnSwitchIfEnforced(oldUserId); if (oldUserId == UserHandle.USER_SYSTEM) { @@ -2200,21 +2198,22 @@ class UserController implements Handler.Callback { @VisibleForTesting void completeUserSwitch(int newUserId) { - if (isUserSwitchUiEnabled()) { - // If there is no challenge set, dismiss the keyguard right away - if (!mInjector.getKeyguardManager().isDeviceSecure(newUserId)) { - // Wait until the keyguard is dismissed to unfreeze - mInjector.dismissKeyguard( - new Runnable() { - public void run() { - unfreezeScreen(); - } - }, - "User Switch"); - return; - } else { + final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled(); + final Runnable runnable = () -> { + if (isUserSwitchUiEnabled) { unfreezeScreen(); } + mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); + mHandler.sendMessage(mHandler.obtainMessage( + REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0)); + }; + + // If there is no challenge set, dismiss the keyguard right away + if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) { + // Wait until the keyguard is dismissed to unfreeze + mInjector.dismissKeyguard(runnable, "User Switch"); + } else { + runnable.run(); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 0eaa5e452a58..1225d9956cd3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4413,7 +4413,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub * a stylus deviceId is not already registered on device. */ @BinderThread - @EnforcePermission(Manifest.permission.INJECT_EVENTS) + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) @Override public void addVirtualStylusIdForTestSession(IInputMethodClient client) { int uid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 80c980310664..a64bd694605c 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -469,6 +469,48 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } /** + * Records that a particular container has been reparented. This only effects windows that have + * already been collected in the transition. This should be called before reparenting because + * the old parent may be removed during reparenting, for example: + * {@link Task#shouldRemoveSelfOnLastChildRemoval} + */ + void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) { + if (!mChanges.containsKey(wc)) { + // #collectReparentChange() will be called when the window is reparented. Skip if it is + // a window that has not been collected, which means we don't care about this window for + // the current transition. + return; + } + final ChangeInfo change = mChanges.get(wc); + // Use the current common ancestor if there are multiple reparent, and the original parent + // has been detached. Otherwise, use the original parent before the transition. + final WindowContainer prevParent = + change.mStartParent == null || change.mStartParent.isAttached() + ? change.mStartParent + : change.mCommonAncestor; + if (prevParent == null || !prevParent.isAttached()) { + Slog.w(TAG, "Trying to collect reparenting of a window after the previous parent has" + + " been detached: " + wc); + return; + } + if (prevParent == newParent) { + Slog.w(TAG, "Trying to collect reparenting of a window that has not been reparented: " + + wc); + return; + } + if (!newParent.isAttached()) { + Slog.w(TAG, "Trying to collect reparenting of a window that is not attached after" + + " reparenting: " + wc); + return; + } + WindowContainer ancestor = newParent; + while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) { + ancestor = ancestor.getParent(); + } + change.mCommonAncestor = ancestor; + } + + /** * @return {@code true} if `wc` is a participant or is a descendant of one. */ boolean isInTransition(WindowContainer wc) { @@ -830,8 +872,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe void abort() { // This calls back into itself via controller.abort, so just early return here. if (mState == STATE_ABORT) return; - if (mState != STATE_COLLECTING) { - throw new IllegalStateException("Too late to abort."); + if (mState != STATE_COLLECTING && mState != STATE_STARTED) { + throw new IllegalStateException("Too late to abort. state=" + mState); } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Aborting Transition: %d", mSyncId); mState = STATE_ABORT; @@ -1524,20 +1566,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return out; } - // Find the top-most shared ancestor of app targets. - WindowContainer<?> ancestor = topApp.getParent(); - // Go up ancestor parent chain until all targets are descendants. - ancestorLoop: - while (ancestor != null) { - for (int i = sortedTargets.size() - 1; i >= 0; --i) { - final WindowContainer wc = sortedTargets.get(i); - if (!isWallpaper(wc) && !wc.isDescendantOf(ancestor)) { - ancestor = ancestor.getParent(); - continue ancestorLoop; - } - } - break; - } + WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp); // make leash based on highest (z-order) direct child of ancestor with a participant. WindowContainer leashReference = sortedTargets.get(0); @@ -1654,6 +1683,46 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return out; } + /** + * Finds the top-most common ancestor of app targets. + * + * Makes sure that the previous parent is also a descendant to make sure the animation won't + * be covered by other windows below the previous parent. For example, when reparenting an + * activity from PiP Task to split screen Task. + */ + @NonNull + private static WindowContainer<?> findCommonAncestor( + @NonNull ArrayList<WindowContainer> targets, + @NonNull ArrayMap<WindowContainer, ChangeInfo> changes, + @NonNull WindowContainer<?> topApp) { + WindowContainer<?> ancestor = topApp.getParent(); + // Go up ancestor parent chain until all targets are descendants. Ancestor should never be + // null because all targets are attached. + for (int i = targets.size() - 1; i >= 0; i--) { + final WindowContainer wc = targets.get(i); + if (isWallpaper(wc)) { + // Skip the non-app window. + continue; + } + while (!wc.isDescendantOf(ancestor)) { + ancestor = ancestor.getParent(); + } + + // Make sure the previous parent is also a descendant to make sure the animation won't + // be covered by other windows below the previous parent. For example, when reparenting + // an activity from PiP Task to split screen Task. + final ChangeInfo change = changes.get(wc); + final WindowContainer prevParent = change.mCommonAncestor; + if (prevParent == null || !prevParent.isAttached()) { + continue; + } + while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) { + ancestor = ancestor.getParent(); + } + } + return ancestor; + } + private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type, ArrayList<WindowContainer> sortedTargets) { // Find the layout params of the top-most application window that is part of the @@ -1772,10 +1841,19 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe @Retention(RetentionPolicy.SOURCE) @interface Flag {} - // Usually "post" change state. + /** + * "Parent" that is also included in the transition. When populating the parent changes, we + * may skip the intermediate parents, so this may not be the actual parent in the hierarchy. + */ WindowContainer mEndParent; - // Parent before change state. + /** Actual parent window before change state. */ WindowContainer mStartParent; + /** + * When the window is reparented during the transition, this is the common ancestor window + * of the {@link #mStartParent} and the current parent. This is needed because the + * {@link #mStartParent} may have been detached when the transition starts. + */ + WindowContainer mCommonAncestor; // State tracking boolean mExistenceChanged = false; diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index ac85c9a36bcc..37bef3a833ee 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -533,6 +533,17 @@ class TransitionController { mCollectingTransition.collectVisibleChange(wc); } + /** + * Records that a particular container has been reparented. This only effects windows that have + * already been collected in the transition. This should be called before reparenting because + * the old parent may be removed during reparenting, for example: + * {@link Task#shouldRemoveSelfOnLastChildRemoval} + */ + void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) { + if (!isCollecting()) return; + mCollectingTransition.collectReparentChange(wc, newParent); + } + /** @see Transition#mStatusBarTransitionDelay */ void setStatusBarTransitionDelay(long delay) { if (mCollectingTransition == null) return; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 0b5de85c5cab..73d4496bdeb5 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -542,6 +542,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< throw new IllegalArgumentException("WC=" + this + " already child of " + mParent); } + // Collect before removing child from old parent, because the old parent may be removed if + // this is the last child in it. + mTransitionController.collectReparentChange(this, newParent); + // The display object before reparenting as that might lead to old parent getting removed // from the display if it no longer has any child. final DisplayContent prevDc = oldParent.getDisplayContent(); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 5f25e3df8a9a..dcf094f99aae 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -83,8 +83,9 @@ public class CredentialManagerUi { */ public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { Log.i(TAG, "In show"); - Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, - mResultReceiver); + Intent intent = IntentFactory.newIntent( + requestInfo, providerDataList, + new ArrayList<>(), mResultReceiver); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 24610df7ad79..ff2107a95d25 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -20,7 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.slice.Slice; import android.credentials.ui.Entry; -import android.credentials.ui.ProviderData; +import android.credentials.ui.GetCredentialProviderData; import android.service.credentials.Action; import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; @@ -117,7 +117,7 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp } @Override - protected final ProviderData prepareUiData() throws IllegalArgumentException { + protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { Log.i(TAG, "In prepareUiData"); if (!ProviderSession.isCompletionStatus(getStatus())) { Log.i(TAG, "In prepareUiData not complete"); @@ -147,7 +147,7 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp * To be called by {@link ProviderGetSession} when the UI is to be invoked. */ @Nullable - private ProviderData prepareUiProviderDataWithCredentials(@NonNull + private GetCredentialProviderData prepareUiProviderDataWithCredentials(@NonNull CredentialsDisplayContent content) { Log.i(TAG, "in prepareUiProviderData"); List<Entry> credentialEntries = new ArrayList<>(); @@ -173,15 +173,10 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp action.getSlice())); } - // TODO : Set the correct last used time - return new ProviderData.Builder(mComponentName.flattenToString(), - mProviderInfo.getServiceLabel() == null ? "" : - mProviderInfo.getServiceLabel().toString(), - /*icon=*/null) + return new GetCredentialProviderData.Builder(mComponentName.flattenToString()) .setCredentialEntries(credentialEntries) .setActionChips(actionChips) .setAuthenticationEntry(authenticationEntry) - .setLastUsedTimeMillis(0) .build(); } @@ -189,7 +184,7 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp * To be called by {@link ProviderGetSession} when the UI is to be invoked. */ @Nullable - private ProviderData prepareUiProviderDataWithAuthentication(@NonNull + private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull Action authenticationEntry) { // TODO : Implement authentication flow return null; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index e1a4c1dd7256..de5960363fa5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -280,13 +280,13 @@ public class BroadcastQueueTest { constants.TIMEOUT = 100; constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) { - public boolean shouldSkip(BroadcastRecord r, ResolveInfo info) { + public boolean shouldSkip(BroadcastRecord r, Object o) { // Ignored return false; } - public boolean shouldSkip(BroadcastRecord r, BroadcastFilter filter) { + public String shouldSkipMessage(BroadcastRecord r, Object o) { // Ignored - return false; + return null; } }; final BroadcastHistory emptyHistory = new BroadcastHistory(constants) { diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index d4c9087a9a69..35b9710f5528 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1520,6 +1520,29 @@ public class TransitionTests extends WindowTestsBase { transition.abort(); } + @Test + public void testCollectReparentChange() { + registerTestTransitionPlayer(); + + // Reparent activity in transition. + final Task lastParent = createTask(mDisplayContent); + final Task newParent = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(lastParent); + doReturn(true).when(lastParent).shouldRemoveSelfOnLastChildRemoval(); + doNothing().when(activity).setDropInputMode(anyInt()); + activity.mVisibleRequested = true; + + final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */, + activity.mTransitionController, mWm.mSyncEngine); + activity.mTransitionController.moveToCollecting(transition); + transition.collect(activity); + activity.reparent(newParent, POSITION_TOP); + + // ChangeInfo#mCommonAncestor should be set after reparent. + final Transition.ChangeInfo change = transition.mChanges.get(activity); + assertEquals(newParent.getDisplayArea(), change.mCommonAncestor); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 2c7867cfd1a2..7be40b873bf8 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8774,6 +8774,22 @@ public class CarrierConfigManager { "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long"; /** + * The amount of time in milliseconds within which the network must set up a slicing + * configuration for the premium capability after + * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} + * returns {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS}. + * During the setup time, calls to + * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} will return + * {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}. + * If the network fails set up a slicing configuration for the premium capability within the + * setup time, subsequent purchase requests will be allowed to go through again. + * + * The default value is 5 minutes. + */ + public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = + "premium_capability_network_setup_time_millis_long"; + + /** * The URL to redirect to when the user clicks on the notification for a network boost via * premium capabilities after applications call * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}. @@ -9496,6 +9512,8 @@ public class CarrierConfigManager { sDefaults.putLong( KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); + sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG, + TimeUnit.MINUTES.toMillis(5)); sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null); sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false); sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{ diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index ef693b5278a0..76a145ce31cc 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -4002,6 +4002,10 @@ public class SubscriptionManager { * cautiously, for example, after formatting the number to a consistent format with * {@link android.telephony.PhoneNumberUtils#formatNumberToE164(String, String)}. * + * <p>The availability and correctness of the phone number depends on the underlying source + * and the network etc. Additional verification is needed to use this number for + * security-related or other sensitive scenarios. + * * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID} * for the default one. * @return the phone number, or an empty string if not available. diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 5f1d08668de1..35b205592a6a 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17206,7 +17206,13 @@ public class TelephonyManager { } /** - * Purchase premium capability request was successful. Subsequent attempts will return + * Purchase premium capability request was successful. + * Once the purchase result is successful, the network must set up a slicing configuration + * for the purchased premium capability within the timeout specified by + * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG}. + * During the setup time, subsequent attempts will return + * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}. + * After setup is complete, subsequent attempts will return * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} until the booster expires. * The expiry time is determined by the type or duration of boost purchased from the carrier, * provided at {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING}. @@ -17330,6 +17336,16 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; /** + * Purchase premium capability was successful and is waiting for the network to setup the + * slicing configuration. If the setup is complete within the time specified by + * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG}, + * subsequent requests will return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} + * until the purchase expires. If the setup is not complete within the time specified above, + * applications can reques the premium capability again. + */ + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; + + /** * Results of the purchase premium capability request. * @hide */ @@ -17347,7 +17363,8 @@ public class TelephonyManager { PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED, PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE, PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED, - PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA}) + PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA, + PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}) public @interface PurchasePremiumCapabilityResult {} /** @@ -17388,6 +17405,8 @@ public class TelephonyManager { return "NETWORK_CONGESTED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA: return "NOT_DEFAULT_DATA"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP: + return "PENDING_NETWORK_SETUP"; default: return "UNKNOWN (" + result + ")"; } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt index 09d7637ffefc..0edbc86ab65f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt @@ -97,8 +97,8 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) : super.statusBarLayerPositionAtEnd() /** {@inheritDoc} */ - @Postsubmit @Test + @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end") override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt index c10b99317b19..4ee12837fe09 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt @@ -16,6 +16,7 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice @@ -71,7 +72,7 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet } @Test - @Postsubmit + @FlakyTest(bugId = 227143265) fun showWhenLockedAppWindowBecomesVisible() { testSpec.assertWm { this.hasNoVisibleAppWindow() @@ -83,7 +84,7 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet } @Test - @Postsubmit + @FlakyTest(bugId = 227143265) fun showWhenLockedAppLayerBecomesVisible() { testSpec.assertLayers { this.isInvisible(showWhenLockedApp) @@ -98,11 +99,17 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet @Presubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() /** {@inheritDoc} */ - @Postsubmit + @FlakyTest(bugId = 227143265) @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() + /** {@inheritDoc} */ + @FlakyTest(bugId = 209599395) + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + companion object { /** * Creates the test configurations. |