diff options
24 files changed, 851 insertions, 372 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 67cb86e21679..7a4624af743b 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -3882,7 +3882,9 @@ public class Activity extends ContextThemeWrapper * it will set up the dispatch to call {@link #onKeyUp} where the action * will be performed; for earlier applications, it will perform the * action immediately in on-down, as those versions of the platform - * behaved. + * behaved. This implementation will also take care of {@link KeyEvent#KEYCODE_ESCAPE} + * by finishing the activity if it would be closed by touching outside + * of it. * * <p>Other additional default key handling may be performed * if configured with {@link #setDefaultKeyMode}. @@ -3904,6 +3906,11 @@ public class Activity extends ContextThemeWrapper return true; } + if (keyCode == KeyEvent.KEYCODE_ESCAPE && mWindow.shouldCloseOnTouchOutside()) { + event.startTracking(); + return true; + } + if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) { return false; } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) { @@ -3999,6 +4006,15 @@ public class Activity extends ContextThemeWrapper return true; } } + + if (keyCode == KeyEvent.KEYCODE_ESCAPE + && mWindow.shouldCloseOnTouchOutside() + && event.isTracking() + && !event.isCanceled()) { + finish(); + return true; + } + return false; } diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 411d157fa927..4851279eea97 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -694,12 +694,22 @@ public class Dialog implements DialogInterface, Window.Callback, */ @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { - if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) - && event.isTracking() - && !event.isCanceled() - && !WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { - onBackPressed(); - return true; + if (event.isTracking() && !event.isCanceled()) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { + onBackPressed(); + return true; + } + break; + case KeyEvent.KEYCODE_ESCAPE: + if (mCancelable) { + cancel(); + } else { + dismiss(); + } + return true; + } } return false; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5c79f697ba13..f7af56f25361 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -17991,6 +17991,15 @@ public final class Settings { "review_permissions_notification_state"; /** + * Whether repair mode is active on the device. + * <p> + * Set to 1 for true and 0 for false. + * + * @hide + */ + public static final String REPAIR_MODE_ACTIVE = "repair_mode_active"; + + /** * Settings migrated from Wear OS settings provider. * @hide */ diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 21fe87f42e1b..7596459b130b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -1482,6 +1482,11 @@ public abstract class Window { } /** @hide */ + public boolean shouldCloseOnTouchOutside() { + return mCloseOnTouchOutside; + } + + /** @hide */ @SuppressWarnings("HiddenAbstractMethod") @UnsupportedAppUsage public abstract void alwaysReadCloseOnTouchAttr(); diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 4ef9efddac79..7535e1bec073 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6459,4 +6459,9 @@ <!-- Whether the AOSP support for app cloning building blocks is to be enabled for the device. --> <bool name="config_enableAppCloningBuildingBlocks">true</bool> + + <!-- Enables or disables support for repair mode. The feature creates a secure + environment to protect the user's privacy when the device is being repaired. + Off by default, since OEMs may have had a similar feature on their devices. --> + <bool name="config_repairModeSupported">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d374bddddc73..c446c3e17288 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4912,6 +4912,8 @@ <java-symbol type="bool" name="config_safetyProtectionEnabled" /> + <java-symbol type="bool" name="config_repairModeSupported" /> + <java-symbol type="string" name="config_devicePolicyManagementUpdater" /> <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" /> diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 701a87f0cce4..9a06be006dca 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -51,9 +51,6 @@ using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { const skcms_ICCProfile* encodedProfile = mCodec->getICCProfile(); if (encodedProfile) { - if (encodedProfile->has_CICP) { - return mCodec->computeOutputColorSpace(kN32_SkColorType); - } // If the profile maps directly to an SkColorSpace, that SkColorSpace // will be returned. Otherwise, nullptr will be returned. In either // case, using this SkColorSpace results in doing no color correction. diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index aae30dfe6223..a0b34690696f 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -144,7 +144,7 @@ android:visibility="gone" android:duplicateParentState="true" android:clickable="false" - android:text="@string/consent_no" /> + android:text="@string/consent_cancel" /> </LinearLayout> diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 2502bbf7b40b..539857951ab1 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -28,13 +28,13 @@ <string name="profile_name_watch">watch</string> <!-- Title of the device selection dialog. --> - <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string> + <string name="chooser_title_non_profile">Choose a device to be managed by <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong></string> - <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string> + <!-- Tile of the multiple devices' dialog. --> + <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to set up</string> - <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> + <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile [CHAR LIMIT=NONE] --> + <string name="summary_watch">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_GLASSES ================= --> @@ -42,13 +42,10 @@ <string name="confirmation_title_glasses">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage <strong><xliff:g id="device_name" example="Glasses">%2$s</xliff:g></strong>?</string> <!-- The name of the "glasses" device type [CHAR LIMIT=30] --> - <string name="profile_name_glasses">glasses</string> + <string name="profile_name_glasses">device</string> - <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string> - - <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> + <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile [CHAR LIMIT=NONE] --> + <string name="summary_glasses">This app will be allowed to access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> @@ -97,9 +94,6 @@ <string name="profile_name_generic">device</string> <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g></string> - - <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] --> <string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device</string> <!-- ================= Buttons ================= --> @@ -110,6 +104,9 @@ <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] --> <string name="consent_no">Don\u2019t allow</string> + <!-- Cancel button for the device chooser dialog [CHAR LIMIT=30] --> + <string name="consent_cancel">Cancel</string> + <!-- Back button for the helper consent dialog [CHAR LIMIT=30] --> <string name="consent_back">Back</string> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index e85190be0e1e..222877bbe9e9 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -69,11 +69,13 @@ <style name="PositiveButton" parent="@android:style/Widget.Material.Button.Borderless.Colored"> - <item name="android:layout_width">300dp</item> - <item name="android:layout_height">56dp</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginBottom">2dp</item> <item name="android:textAllCaps">false</item> <item name="android:textSize">14sp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> <item name="android:textColor">@android:color/system_neutral1_900</item> <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> <item name="android:background">@drawable/btn_positive_bottom</item> @@ -81,11 +83,13 @@ <style name="NegativeButton" parent="@android:style/Widget.Material.Button.Borderless.Colored"> - <item name="android:layout_width">300dp</item> - <item name="android:layout_height">56dp</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginTop">2dp</item> <item name="android:textAllCaps">false</item> <item name="android:textSize">14sp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> <item name="android:textColor">@android:color/system_neutral1_900</item> <item name="android:layout_marginTop">4dp</item> <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index 4154029b6d41..97016f5384f6 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -27,10 +27,8 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; -import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME; -import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON; import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; @@ -121,6 +119,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements private IAssociationRequestCallback mAppCallback; private ResultReceiver mCdmServiceReceiver; + // Present for application's name. + private CharSequence mAppLabel; + // Always present widgets. private TextView mTitle; private TextView mSummary; @@ -165,8 +166,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements private @Nullable RecyclerView mDeviceListRecyclerView; private @Nullable DeviceListAdapter mDeviceAdapter; - - // The recycler view is only shown for selfManaged and singleDevice association request. + // The recycler view is shown for non-null profile association request. private @Nullable RecyclerView mPermissionListRecyclerView; private @Nullable PermissionListAdapter mPermissionListAdapter; @@ -178,8 +178,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements // onActivityResult() after the association is created. private @Nullable DeviceFilterPair<?> mSelectedDevice; - private @Nullable List<Integer> mPermissionTypes; - private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); @Override @@ -302,6 +300,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements setContentView(R.layout.activity_confirmation); + mAppLabel = appLabel; + mConstraintList = findViewById(R.id.constraint_list); mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); mVendorHeader = findViewById(R.id.vendor_header); @@ -322,7 +322,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); - mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick); mPermissionListRecyclerView = findViewById(R.id.permission_list); mPermissionListAdapter = new PermissionListAdapter(this); @@ -468,8 +467,6 @@ public class CompanionDeviceActivity extends FragmentActivity implements throw new RuntimeException("Unsupported profile " + deviceProfile); } - mPermissionTypes = new ArrayList<>(); - try { vendorIcon = getVendorHeaderIcon(this, packageName, userId); vendorName = getVendorHeaderName(this, packageName, userId); @@ -486,17 +483,13 @@ public class CompanionDeviceActivity extends FragmentActivity implements } title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName); - mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); + setupPermissionList(deviceProfile); // Summary is not needed for selfManaged dialog. mSummary.setVisibility(View.GONE); - - setupPermissionList(); - mTitle.setText(title); mVendorHeaderName.setText(vendorName); mVendorHeader.setVisibility(View.VISIBLE); - mVendorHeader.setVisibility(View.VISIBLE); mProfileIcon.setVisibility(View.GONE); mDeviceListRecyclerView.setVisibility(View.GONE); // Top and bottom borders should be gone for selfManaged dialog. @@ -509,7 +502,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements final String deviceProfile = mRequest.getDeviceProfile(); - mPermissionTypes = new ArrayList<>(); + if (!SUPPORTED_PROFILES.contains(deviceProfile)) { + throw new RuntimeException("Unsupported profile " + deviceProfile); + } CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> updateSingleDeviceUi( @@ -529,75 +524,40 @@ public class CompanionDeviceActivity extends FragmentActivity implements if (deviceFilterPairs.isEmpty()) return; mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); - // No need to show user consent dialog if it is a singleDevice - // and isSkipPrompt(true) AssociationRequest. - // See AssociationRequestsProcessor#mayAssociateWithoutPrompt. - if (mRequest.isSkipPrompt()) { - mSingleDeviceSpinner.setVisibility(View.GONE); - onUserSelectedDevice(mSelectedDevice); - return; - } - - final String deviceName = mSelectedDevice.getDisplayName(); - final Spanned title; - final Spanned summary; - final Drawable profileIcon; - if (!SUPPORTED_PROFILES.contains(deviceProfile)) { - throw new RuntimeException("Unsupported profile " + deviceProfile); - } + final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); - if (deviceProfile == null) { - summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName); - mConstraintList.setVisibility(View.GONE); - } else { - summary = getHtmlFromResources( - this, SUMMARIES.get(deviceProfile), getString(R.string.device_type)); - mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); - setupPermissionList(); - } - - title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName); - profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); + updatePermissionUi(); - mTitle.setText(title); - mSummary.setText(summary); mProfileIcon.setImageDrawable(profileIcon); - mSingleDeviceSpinner.setVisibility(View.GONE); mAssociationConfirmationDialog.setVisibility(View.VISIBLE); + mSingleDeviceSpinner.setVisibility(View.GONE); } private void initUiForMultipleDevices(CharSequence appLabel) { if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()"); - final String deviceProfile = mRequest.getDeviceProfile(); - - final String profileName; - final String profileNameMulti; - final Spanned summary; final Drawable profileIcon; - final int summaryResourceId; + final Spanned title; + final String deviceProfile = mRequest.getDeviceProfile(); if (!SUPPORTED_PROFILES.contains(deviceProfile)) { throw new RuntimeException("Unsupported profile " + deviceProfile); } - profileName = getString(PROFILES_NAME.get(deviceProfile)); - profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile)); profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); - summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile); if (deviceProfile == null) { - summary = getHtmlFromResources(this, summaryResourceId); + title = getHtmlFromResources(this, R.string.chooser_title_non_profile, appLabel); + mButtonNotAllowMultipleDevices.setText(R.string.consent_no); } else { - summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel); + title = getHtmlFromResources(this, + R.string.chooser_title, getString(PROFILES_NAME.get(deviceProfile))); } - final Spanned title = getHtmlFromResources( - this, R.string.chooser_title, profileNameMulti, appLabel); + mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); mTitle.setText(title); - mSummary.setText(summary); mProfileIcon.setImageDrawable(profileIcon); mDeviceListRecyclerView.setAdapter(mDeviceAdapter); @@ -613,6 +573,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements mDeviceAdapter.setDevices(deviceFilterPairs); }); + mSummary.setVisibility(View.GONE); // "Remove" consent button: users would need to click on the list item. mButtonAllow.setVisibility(View.GONE); mButtonNotAllow.setVisibility(View.GONE); @@ -623,11 +584,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements mMultipleDeviceSpinner.setVisibility(View.VISIBLE); } - private void onListItemClick(int position) { - if (DEBUG) Log.d(TAG, "onListItemClick() " + position); - + private void onDeviceClicked(int position) { final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); - + // To prevent double tap on the selected device. if (mSelectedDevice != null) { if (DEBUG) Log.w(TAG, "Already selected."); return; @@ -637,7 +596,47 @@ public class CompanionDeviceActivity extends FragmentActivity implements mSelectedDevice = requireNonNull(selectedDevice); - onUserSelectedDevice(selectedDevice); + Log.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString()); + + updatePermissionUi(); + + mSummary.setVisibility(View.VISIBLE); + mButtonAllow.setVisibility(View.VISIBLE); + mButtonNotAllow.setVisibility(View.VISIBLE); + mDeviceListRecyclerView.setVisibility(View.GONE); + mNotAllowMultipleDevicesLayout.setVisibility(View.GONE); + } + + private void updatePermissionUi() { + final String deviceProfile = mRequest.getDeviceProfile(); + final int summaryResourceId = SUMMARIES.get(deviceProfile); + final String remoteDeviceName = mSelectedDevice.getDisplayName(); + final Spanned title = getHtmlFromResources( + this, TITLES.get(deviceProfile), mAppLabel, remoteDeviceName); + final Spanned summary; + + // No need to show permission consent dialog if it is a isSkipPrompt(true) + // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt. + if (mRequest.isSkipPrompt()) { + mSingleDeviceSpinner.setVisibility(View.GONE); + onUserSelectedDevice(mSelectedDevice); + return; + } + + if (deviceProfile == null && mRequest.isSingleDevice()) { + summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName); + mConstraintList.setVisibility(View.GONE); + } else if (deviceProfile == null) { + onUserSelectedDevice(mSelectedDevice); + return; + } else { + summary = getHtmlFromResources( + this, summaryResourceId, getString(R.string.device_type)); + setupPermissionList(deviceProfile); + } + + mTitle.setText(title); + mSummary.setText(summary); } private void onPositiveButtonClick(View v) { @@ -680,8 +679,9 @@ public class CompanionDeviceActivity extends FragmentActivity implements // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling // and when mPermissionListRecyclerView is fully populated. // Lastly, disable the Allow and Don't allow buttons. - private void setupPermissionList() { - mPermissionListAdapter.setPermissionType(mPermissionTypes); + private void setupPermissionList(String deviceProfile) { + final List<Integer> permissionTypes = new ArrayList<>(PERMISSION_TYPES.get(deviceProfile)); + mPermissionListAdapter.setPermissionType(permissionTypes); mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java index 7aed13960b08..060c03213bcd 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java @@ -86,21 +86,11 @@ final class CompanionDeviceResources { static final Map<String, Integer> SUMMARIES; static { final Map<String, Integer> map = new ArrayMap<>(); - map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device); - map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device); - map.put(null, R.string.summary_generic_single_device); - - SUMMARIES = unmodifiableMap(map); - } - - static final Map<String, Integer> MULTI_DEVICES_SUMMARIES; - static { - final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch); - map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_multi_device); + map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses); map.put(null, R.string.summary_generic); - MULTI_DEVICES_SUMMARIES = unmodifiableMap(map); + SUMMARIES = unmodifiableMap(map); } static final Map<String, Integer> PROFILES_NAME; diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index ef4b81491ce1..9bb9a5532483 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -592,6 +592,7 @@ public class SettingsBackupTest { Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS, Settings.Global.AUTO_REVOKE_PARAMETERS, Settings.Global.ENABLE_RADIO_BUG_DETECTION, + Settings.Global.REPAIR_MODE_ACTIVE, Settings.Global.RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD, Settings.Global.RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD, Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 2241bf2d6ea1..a35dd3a70fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -688,8 +688,7 @@ object Flags { // TODO(b/259428678): Tracking Bug @JvmField - val KEYBOARD_BACKLIGHT_INDICATOR = - unreleasedFlag(2601, "keyboard_backlight_indicator", teamfood = true) + val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator") // TODO(b/277192623): Tracking Bug @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 42de7f0f3a8b..fcae081f7edd 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -38,6 +38,7 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; +import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; @@ -192,6 +193,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final InputManager mInputManager; private final Optional<Pip> mPipOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final FalsingManager mFalsingManager; @@ -203,6 +205,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final int mDisplayId; private final Executor mMainExecutor; + private final Handler mMainHandler; private final Executor mBackgroundExecutor; private final Rect mPipExcludedBounds = new Rect(); @@ -246,6 +249,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean mIsAttached; private boolean mIsGesturalModeEnabled; + private boolean mIsTrackpadConnected; + private boolean mUsingThreeButtonNav; private boolean mIsEnabled; private boolean mIsNavBarShownTransiently; private boolean mIsBackGestureAllowed; @@ -343,12 +348,48 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } }; + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + + // Only one trackpad can be connected to a device at a time, since it takes over the + // only USB port. + private int mTrackpadDeviceId; + + @Override + public void onInputDeviceAdded(int deviceId) { + if (isTrackpadDevice(deviceId)) { + mTrackpadDeviceId = deviceId; + update(true /* isTrackpadConnected */); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (mTrackpadDeviceId == deviceId) { + update(false /* isTrackpadConnected */); + } + } + + private void update(boolean isTrackpadConnected) { + boolean isPreviouslyTrackpadConnected = mIsTrackpadConnected; + mIsTrackpadConnected = isTrackpadConnected; + if (isPreviouslyTrackpadConnected != mIsTrackpadConnected) { + updateIsEnabled(); + updateCurrentUserResources(); + } + } + }; + EdgeBackGestureHandler( Context context, OverviewProxyService overviewProxyService, SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor, + @Main Handler handler, @Background Executor backgroundExecutor, UserTracker userTracker, ProtoTracer protoTracer, @@ -357,6 +398,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + InputManager inputManager, Optional<Pip> pipOptional, Optional<DesktopMode> desktopModeOptional, FalsingManager falsingManager, @@ -367,6 +409,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = executor; + mMainHandler = handler; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mOverviewProxyService = overviewProxyService; @@ -378,6 +421,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mInputManager = inputManager; mPipOptional = pipOptional; mDesktopModeOptional = desktopModeOptional; mFalsingManager = falsingManager; @@ -386,6 +430,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mFeatureFlags = featureFlags; mLightBarControllerProvider = lightBarControllerProvider; mLastReportedConfig.setTo(mContext.getResources().getConfiguration()); + mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled( + Flags.TRACKPAD_GESTURE_FEATURES); ComponentName recentsComponentName = ComponentName.unflattenFromString( context.getString(com.android.internal.R.string.config_recentsComponentName)); if (recentsComponentName != null) { @@ -417,7 +463,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged); + mMainHandler, mContext, this::onNavigationSettingsChanged); updateCurrentUserResources(); } @@ -496,6 +542,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mProtoTracer.add(this); mOverviewProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); + if (mIsTrackpadGestureFeaturesEnabled) { + mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler); + int [] inputDevices = mInputManager.getInputDeviceIds(); + for (int inputDeviceId : inputDevices) { + if (isTrackpadDevice(inputDeviceId)) { + mIsTrackpadConnected = true; + } + } + } updateIsEnabled(); mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } @@ -508,6 +563,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mProtoTracer.remove(this); mOverviewProxyService.removeCallback(mQuickSwitchListener); mSysUiState.removeCallback(mSysUiStateCallback); + mInputManager.unregisterInputDeviceListener(mInputDeviceListener); updateIsEnabled(); mUserTracker.removeCallback(mUserChangedCallback); } @@ -516,7 +572,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged */ public void onNavigationModeChanged(int mode) { - mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); + mUsingThreeButtonNav = QuickStepContract.isLegacyMode(mode); + mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode) || ( + mIsTrackpadGestureFeaturesEnabled && mUsingThreeButtonNav && mIsTrackpadConnected); updateIsEnabled(); updateCurrentUserResources(); } @@ -606,8 +664,6 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Add a nav bar panel window mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE); - mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled( - Flags.TRACKPAD_GESTURE_FEATURES); resetEdgeBackPlugin(); mPluginManager.addPluginListener( this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false); @@ -811,6 +867,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mDisplaySize.y - insets.bottom); } + private boolean isTrackpadDevice(int deviceId) { + InputDevice inputDevice = mInputManager.getInputDevice(deviceId); + return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE + | InputDevice.SOURCE_TOUCHPAD); + } + private boolean desktopExcludeRegionContains(int x, int y) { return mDesktopModeExcludeRegion.contains(x, y); } @@ -935,17 +997,20 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mMLResults = 0; mLogGesture = false; mInRejectedExclusion = false; - // Trackpad back gestures don't have zones, so we don't need to check if the down event - // is within insets. Also we don't allow back for button press from the trackpad, and - // yet we do with a mouse. boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY()); - mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed - && !isButtonPressFromTrackpad(ev) - && (isTrackpadMultiFingerSwipe || isWithinInsets) + boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed && !mGestureBlockingActivityRunning - && !QuickStepContract.isBackGestureDisabled(mSysUiFlags) - && (isValidTrackpadBackGesture(isTrackpadMultiFingerSwipe) - || isWithinTouchRegion((int) ev.getX(), (int) ev.getY())); + && !QuickStepContract.isBackGestureDisabled(mSysUiFlags); + if (isTrackpadMultiFingerSwipe) { + // Trackpad back gestures don't have zones, so we don't need to check if the down + // event is within insets. + mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture( + isTrackpadMultiFingerSwipe); + } else { + mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets + && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()) + && !isButtonPressFromTrackpad(ev); + } if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); mEdgeBackPlugin.onMotionEvent(ev); @@ -1048,6 +1113,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private boolean isButtonPressFromTrackpad(MotionEvent ev) { + // We don't allow back for button press from the trackpad, and yet we do with a mouse. int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources(); return (sources & (SOURCE_MOUSE | SOURCE_TOUCHPAD)) == sources && ev.getButtonState() != 0; } @@ -1170,6 +1236,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack pw.println(" mPredictionLog=" + String.join("\n", mPredictionLog)); pw.println(" mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets)); pw.println(" mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets)); + pw.println(" mIsTrackpadConnected=" + mIsTrackpadConnected); + pw.println(" mUsingThreeButtonNav=" + mUsingThreeButtonNav); pw.println(" mEdgeBackPlugin=" + mEdgeBackPlugin); if (mEdgeBackPlugin != null) { mEdgeBackPlugin.dump(pw); @@ -1219,6 +1287,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final SysUiState mSysUiState; private final PluginManager mPluginManager; private final Executor mExecutor; + private final Handler mHandler; private final Executor mBackgroundExecutor; private final UserTracker mUserTracker; private final ProtoTracer mProtoTracer; @@ -1227,6 +1296,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final InputManager mInputManager; private final Optional<Pip> mPipOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final FalsingManager mFalsingManager; @@ -1241,6 +1311,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor, + @Main Handler handler, @Background Executor backgroundExecutor, UserTracker userTracker, ProtoTracer protoTracer, @@ -1249,6 +1320,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + InputManager inputManager, Optional<Pip> pipOptional, Optional<DesktopMode> desktopModeOptional, FalsingManager falsingManager, @@ -1261,6 +1333,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mSysUiState = sysUiState; mPluginManager = pluginManager; mExecutor = executor; + mHandler = handler; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mProtoTracer = protoTracer; @@ -1269,6 +1342,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mInputManager = inputManager; mPipOptional = pipOptional; mDesktopModeOptional = desktopModeOptional; mFalsingManager = falsingManager; @@ -1286,6 +1360,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mSysUiState, mPluginManager, mExecutor, + mHandler, mBackgroundExecutor, mUserTracker, mProtoTracer, @@ -1294,6 +1369,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mViewConfiguration, mWindowManager, mWindowManagerService, + mInputManager, mPipOptional, mDesktopModeOptional, mFalsingManager, diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java new file mode 100644 index 000000000000..3854adad9cd8 --- /dev/null +++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.sysprop.InputProperties; + +import java.util.Optional; + +/** + * A component of {@link InputManagerService} responsible for managing the input sysprop flags + * + * @hide + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public final class InputFeatureFlagProvider { + + // To disable Keyboard backlight control via Framework, run: + // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = + InputProperties.enable_keyboard_backlight_control().orElse(true); + + // To disable Framework controlled keyboard backlight animation run: + // adb shell setprop persist.input.keyboard.backlight_animation.enabled false (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = + InputProperties.enable_keyboard_backlight_animation().orElse(false); + + private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); + private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); + + public static boolean isKeyboardBacklightControlEnabled() { + return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); + } + + public static boolean isKeyboardBacklightAnimationEnabled() { + return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED); + } + + public static void setKeyboardBacklightControlEnabled(boolean enabled) { + sKeyboardBacklightControlOverride = Optional.of(enabled); + } + + public static void setKeyboardBacklightAnimationEnabled(boolean enabled) { + sKeyboardBacklightAnimationOverride = Optional.of(enabled); + } + + /** + * Clears all input feature flag overrides. + */ + public static void clearOverrides() { + sKeyboardBacklightControlOverride = Optional.empty(); + sKeyboardBacklightAnimationOverride = Optional.empty(); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 662591e3d264..9f3ab885bf16 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -74,7 +74,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; @@ -160,11 +159,6 @@ public class InputManagerService extends IInputManager.Stub private static final AdditionalDisplayInputProperties DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); - // To disable Keyboard backlight control via Framework, run: - // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) - private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( - "persist.input.keyboard.backlight_control.enabled", true); - private final NativeInputManagerService mNative; private final Context mContext; @@ -439,10 +433,9 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); - mKeyboardBacklightController = - KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext, - mNative, mDataStore, injector.getLooper()) - : new KeyboardBacklightControllerInterface() {}; + mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled() + ? new KeyboardBacklightController(mContext, mNative, mDataStore, + injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 48c346a2fe22..61ca0cbff7bf 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -16,6 +16,7 @@ package com.android.server.input; +import android.animation.ValueAnimator; import android.annotation.BinderThread; import android.content.Context; import android.graphics.Color; @@ -70,6 +71,8 @@ final class KeyboardBacklightController implements private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; private static final int MAX_BRIGHTNESS = 255; private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; + private static final long TRANSITION_ANIMATION_DURATION_MILLIS = + Duration.ofSeconds(1).toMillis(); private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; @@ -85,6 +88,7 @@ final class KeyboardBacklightController implements @GuardedBy("mDataStore") private final PersistentDataStore mDataStore; private final Handler mHandler; + private final AnimatorFactory mAnimatorFactory; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off @@ -109,10 +113,17 @@ final class KeyboardBacklightController implements KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper) { + this(context, nativeService, dataStore, looper, ValueAnimator::ofInt); + } + + @VisibleForTesting + KeyboardBacklightController(Context context, NativeInputManagerService nativeService, + PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); + mAnimatorFactory = animatorFactory; } @Override @@ -177,8 +188,7 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } - updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel, - true /* isTriggeredByKeyPress */); + updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); synchronized (mDataStore) { try { @@ -203,8 +213,7 @@ final class KeyboardBacklightController implements if (index < 0) { index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1)); } - updateBacklightState(inputDevice.getId(), keyboardBacklight, index, - false /* isTriggeredByKeyPress */); + updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } @@ -217,14 +226,10 @@ final class KeyboardBacklightController implements if (!mIsInteractive) { return; } - if (!mIsBacklightOn) { - mIsBacklightOn = true; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = true; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, @@ -232,14 +237,10 @@ final class KeyboardBacklightController implements } private void handleUserInactivity() { - if (mIsBacklightOn) { - mIsBacklightOn = false; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = false; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } } @@ -310,7 +311,7 @@ final class KeyboardBacklightController implements return; } // The keyboard backlight was added or changed. - mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight)); + mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); restoreBacklightBrightness(inputDevice, keyboardBacklight); } @@ -372,21 +373,14 @@ final class KeyboardBacklightController implements } } - private void updateBacklightState(int deviceId, Light light, int brightnessLevel, + private void updateBacklightState(int deviceId, int brightnessLevel, boolean isTriggeredByKeyPress) { KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); if (state == null) { return; } - mNative.setLightColor(deviceId, light.getId(), - mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0) - : 0); - if (DEBUG) { - Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel - + "(isBacklightOn = " + mIsBacklightOn + ")"); - } - state.mBrightnessLevel = brightnessLevel; + state.setBrightnessLevel(brightnessLevel); synchronized (mKeyboardBacklightListenerRecords) { for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { @@ -397,6 +391,10 @@ final class KeyboardBacklightController implements deviceId, callbackState, isTriggeredByKeyPress); } } + + if (DEBUG) { + Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); + } } private void onKeyboardBacklightListenerDied(int pid) { @@ -436,10 +434,7 @@ final class KeyboardBacklightController implements @Override public void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); - ipw.println( - TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = " - + mIsBacklightOn); - + ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); ipw.increaseIndent(); for (int i = 0; i < mKeyboardBacklights.size(); i++) { KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); @@ -448,6 +443,10 @@ final class KeyboardBacklightController implements ipw.decreaseIndent(); } + private static boolean isAnimationEnabled() { + return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled(); + } + // A record of a registered Keyboard backlight listener from one process. private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { public final int mPid; @@ -478,14 +477,55 @@ final class KeyboardBacklightController implements } } - private static class KeyboardBacklightState { + private class KeyboardBacklightState { + private final int mDeviceId; private final Light mLight; private int mBrightnessLevel; + private ValueAnimator mAnimator; - KeyboardBacklightState(Light light) { + KeyboardBacklightState(int deviceId, Light light) { + mDeviceId = deviceId; mLight = light; } + private void onBacklightStateChanged() { + setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0); + } + private void setBrightnessLevel(int brightnessLevel) { + if (mIsBacklightOn) { + setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]); + } + mBrightnessLevel = brightnessLevel; + } + + private void cancelAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.cancel(); + } + } + + private void setBacklightValue(int toValue) { + int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); + if (fromValue == toValue) { + return; + } + if (isAnimationEnabled()) { + startAnimation(fromValue, toValue); + } else { + mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); + } + } + + private void startAnimation(int fromValue, int toValue) { + // Cancel any ongoing animation before starting a new one + cancelAnimation(); + mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); + mAnimator.addUpdateListener( + (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), + Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); + mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); + } + @Override public String toString() { return "KeyboardBacklightState{Light=" + mLight.getId() @@ -493,4 +533,9 @@ final class KeyboardBacklightController implements + "}"; } } + + @VisibleForTesting + interface AnimatorFactory { + ValueAnimator makeIntAnimator(int from, int to); + } } diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 64c05dc8ab84..272679280a62 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -16,6 +16,7 @@ package com.android.server.input +import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper import android.graphics.Color @@ -29,6 +30,7 @@ import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice +import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS @@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests { private lateinit var context: Context private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper + private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 + private var lastAnimationValues = IntArray(2) @Before fun setup() { @@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests { override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} }) testLooper = TestLooper() - keyboardBacklightController = - KeyboardBacklightController(context, native, dataStore, testLooper.looper) + keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, + testLooper.looper, FakeAnimatorFactory()) InputManagerGlobal.resetInstance(iInputManager) val inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) @@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests { val args = it.arguments lightColorMap.put(args[1] as Int, args[2] as Int) } + `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { + val args = it.arguments + lightColorMap.getOrDefault(args[1] as Int, 0) + } lightColorMap.clear() `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ @@ -138,271 +146,287 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { + for (level in 1 until totalLevels) { + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + // Increment above max level incrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for max level mismatched", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for max level must be correctly stored in the datastore", + MAX_BRIGHTNESS, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) - } - // Increment above max level - incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for max level mismatched", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for max level must be correctly stored in the datastore", - MAX_BRIGHTNESS, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) + for (level in totalLevels - 2 downTo 0) { + decrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } - for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) { + // Decrement below min level decrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for min level mismatched", + Color.argb(0, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for min level must be correctly stored in the datastore", + 0, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) } - - // Decrement below min level - decrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for min level mismatched", - Color.argb(0, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for min level must be correctly stored in the datastore", - 0, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) } @Test fun testKeyboardWithoutBacklight() { - val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) - val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + BacklightAnimationFlag(false).use { + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + } } @Test fun testKeyboardWithMultipleLight() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( - listOf( - keyboardBacklight, - keyboardInputLight + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight + ) ) - ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - incrementKeyboardBacklight(DEVICE_ID) - assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) - assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) - assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + incrementKeyboardBacklight(DEVICE_ID) + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + } } @Test fun testRestoreBacklightOnInputDeviceAdded() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { - dataStore.setKeyboardBacklightBrightness( + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + for (level in 1 until totalLevels) { + dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1 - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store", + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the " + + "data store", Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), lightColorMap[LIGHT_ID] - ) - keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + ) + keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + } } } @Test fun testRestoreBacklightOnInputDeviceChanged() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertTrue( - "Keyboard backlight should not be changed until its added", - lightColorMap.isEmpty() - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertTrue( + "Keyboard backlight should not be changed until its added", + lightColorMap.isEmpty() + ) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_registerUnregisterListener() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - // Register backlight listener - val listener = KeyboardBacklightListener() - keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) - lastBacklightState = null - keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) - testLooper.dispatchNext() + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() - assertEquals( - "Backlight state device Id should be $DEVICE_ID", - DEVICE_ID, - lastBacklightState!!.deviceId - ) - assertEquals( - "Backlight state brightnessLevel should be " + 1, - 1, - lastBacklightState!!.brightnessLevel - ) - assertEquals( - "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - lastBacklightState!!.maxBrightnessLevel - ) - assertEquals( - "Backlight state isTriggeredByKeyPress should be true", - true, - lastBacklightState!!.isTriggeredByKeyPress - ) + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be " + 1, + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be " + (totalLevels - 1), + (totalLevels - 1), + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) - // Unregister listener - keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) - lastBacklightState = null - incrementKeyboardBacklight(DEVICE_ID) + lastBacklightState = null + incrementKeyboardBacklight(DEVICE_ID) - assertNull("Listener should not receive any updates", lastBacklightState) + assertNull("Listener should not receive any updates", lastBacklightState) + } } @Test fun testKeyboardBacklight_userActivity() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be turned off after inactivity", - 0, - lightColorMap[LIGHT_ID] - ) + testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be turned off after inactivity", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_displayOnOff() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store when display turned on", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data " + + "store when display turned on", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be turned off after display is turned off", - 0, - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned off after display is turned off", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test @@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests { ) } + @Test + @UiThreadTest + fun testKeyboardBacklightAnimation_onChangeLevels() { + BacklightAnimationFlag(true).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Should start animation from level 0", + BRIGHTNESS_VALUE_FOR_LEVEL[0], + lastAnimationValues[0] + ) + assertEquals( + "Should start animation to level 1", + BRIGHTNESS_VALUE_FOR_LEVEL[1], + lastAnimationValues[1] + ) + } + } + inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { override fun onBrightnessChanged( deviceId: Int, @@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests { val maxBrightnessLevel: Int, val isTriggeredByKeyPress: Boolean ) + + private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable { + init { + InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled) + } + + override fun close() { + InputFeatureFlagProvider.clearOverrides() + } + } + + private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { + override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { + lastAnimationValues[0] = from + lastAnimationValues[1] = to + return ValueAnimator.ofInt(from, to) + } + } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt new file mode 100644 index 000000000000..c0c738b16c2a --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.activityembedding + +import android.platform.test.annotations.Presubmit +import android.tools.common.datatypes.Rect +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test closing a secondary activity in a split. + * + * Setup: Launch A|B in split with B being the secondary activity. + * Transitions: Finish B and expect A to become fullscreen. + * + * To run this test: `atest FlickerTests:CloseSecondaryActivityInSplitTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class CloseSecondaryActivityInSplitTest(flicker: FlickerTest) : + ActivityEmbeddingTestBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit = { + setup { + tapl.setExpectedRotationCheckEnabled(false) + // Launches fullscreen A. + testApp.launchViaIntent(wmHelper) + // Launches a split A|B and waits for both activities to show. + testApp.launchSecondaryActivity(wmHelper) + // Get fullscreen bounds + startDisplayBounds = + wmHelper.currentState.layerState.physicalDisplayBounds ?: + error("Can't get display bounds") + } + transitions { + // Finish secondary activity B. + testApp.finishSecondaryActivity(wmHelper) + // Expect the main activity A to expand into fullscreen. + wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() + } + teardown { + tapl.goHome() + testApp.exit(wmHelper) + } + } + + /** Main activity is always visible and becomes fullscreen in the end. */ + @Presubmit + @Test + fun mainActivityWindowBecomesFullScreen() { + flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } + flicker.assertWmEnd { + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .coversExactly(startDisplayBounds) + } + } + + /** Main activity surface is animated from split to fullscreen. */ + @Presubmit + @Test + fun mainActivityLayerIsAlwaysVisible() { + flicker.assertLayers { + isVisible( + ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or( + ComponentNameMatcher.TRANSITION_SNAPSHOT + ) + ) + } + flicker.assertLayersEnd { + isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT) + } + } + + /** Secondary activity should destroy and become invisible. */ + @Presubmit + @Test + fun secondaryActivityWindowFinishes() { + flicker.assertWm { + contains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + @Presubmit + @Test + fun secondaryActivityLayerFinishes() { + flicker.assertLayers { + isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + companion object { + /** {@inheritDoc} */ + private var startDisplayBounds = Rect.EMPTY + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } + }
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index daecfe7e4c4d..e019b2b22680 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -60,6 +60,24 @@ constructor( } /** + * Clicks the button to finishes the secondary activity launched through + * [launchSecondaryActivity], waits for the main activity to resume. + */ + fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) { + val finishButton = + uiDevice.wait( + Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")), + FIND_TIMEOUT + ) + require(finishButton != null) { "Can't find finish secondary activity button on screen." } + finishButton.click() + wmHelper + .StateSyncBuilder() + .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) + .waitForAndVerify() + } + + /** * Clicks the button to launch the placeholder primary activity, which should launch the * placeholder secondary activity based on the placeholder rule. */ diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml index 3a02cadc90dd..f0dfdfce035f 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml @@ -20,5 +20,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml new file mode 100644 index 000000000000..239aba59f4a7 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/secondary_activity_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Button + android:id="@+id/finish_secondary_activity_button" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:text="Finish" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java index 00f4c2576eb1..6e78750cdeee 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java @@ -16,15 +16,28 @@ package com.android.server.wm.flicker.testapp; +import android.app.Activity; import android.graphics.Color; +import android.os.Bundle; +import android.view.View; /** * Activity to be used as the secondary activity to split with * {@link ActivityEmbeddingMainActivity}. */ -public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity { +public class ActivityEmbeddingSecondaryActivity extends Activity { + @Override - int getBackgroundColor() { - return Color.YELLOW; + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_embedding_secondary_activity_layout); + findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW); + findViewById(R.id.finish_secondary_activity_button).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); } } |