Merge changes from topic "flexi-simpin" into main
* changes:
Add tests for Flexiglass sim pin.
SimPin for Flexiglass
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index b17d2d1..c6601e8d 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -2065,6 +2065,7 @@
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
+ case KeyEvent.KEYCODE_STEM_PRIMARY:
return true;
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index ab9566e..5296b99 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -17,6 +17,13 @@
}
flag {
+ name: "deduplicate_accessibility_warning_dialog"
+ namespace: "accessibility"
+ description: "Removes duplicate definition of the accessibility warning dialog."
+ bug: "303511250"
+}
+
+flag {
namespace: "accessibility"
name: "force_invert_color"
description: "Enable force force-dark for smart inversion and dark theme everywhere"
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
index 6497409..2b6913c 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
@@ -34,6 +34,8 @@
*/
class AccessibilityServiceTarget extends AccessibilityTarget {
+ private final AccessibilityServiceInfo mAccessibilityServiceInfo;
+
AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
@AccessibilityFragmentType int fragmentType,
@NonNull AccessibilityServiceInfo serviceInfo) {
@@ -47,6 +49,7 @@
serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()),
serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()),
convertToKey(convertToUserType(shortcutType)));
+ mAccessibilityServiceInfo = serviceInfo;
}
@Override
@@ -64,4 +67,8 @@
holder.mLabelView.setEnabled(enabled);
holder.mStatusView.setEnabled(enabled);
}
+
+ public AccessibilityServiceInfo getAccessibilityServiceInfo() {
+ return mAccessibilityServiceInfo;
+ }
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
new file mode 100644
index 0000000..0f8ced2
--- /dev/null
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
@@ -0,0 +1,139 @@
+/*
+ * 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.BidiFormatter;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+/**
+ * Utility class for creating the dialog that asks the user for explicit permission
+ * before an accessibility service is enabled.
+ */
+public class AccessibilityServiceWarning {
+
+ /**
+ * Returns an {@link AlertDialog} to be shown to confirm that the user
+ * wants to enable an {@link android.accessibilityservice.AccessibilityService}.
+ */
+ public static AlertDialog createAccessibilityServiceWarningDialog(@NonNull Context context,
+ @NonNull AccessibilityServiceInfo info,
+ @NonNull View.OnClickListener allowListener,
+ @NonNull View.OnClickListener denyListener,
+ @NonNull View.OnClickListener uninstallListener) {
+ final AlertDialog ad = new AlertDialog.Builder(context)
+ .setView(createAccessibilityServiceWarningDialogContentView(
+ context, info, allowListener, denyListener, uninstallListener))
+ .setCancelable(true)
+ .create();
+ Window window = ad.getWindow();
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+ window.setAttributes(params);
+ return ad;
+ }
+
+ @VisibleForTesting
+ public static View createAccessibilityServiceWarningDialogContentView(Context context,
+ AccessibilityServiceInfo info,
+ View.OnClickListener allowListener,
+ View.OnClickListener denyListener,
+ View.OnClickListener uninstallListener) {
+ final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
+ final View content = inflater.inflate(R.layout.accessibility_service_warning, null);
+
+ final Drawable icon;
+ if (info.getResolveInfo().getIconResource() == 0) {
+ icon = context.getDrawable(R.drawable.ic_accessibility_generic);
+ } else {
+ icon = info.getResolveInfo().loadIcon(context.getPackageManager());
+ }
+ final ImageView permissionDialogIcon = content.findViewById(
+ R.id.accessibility_permissionDialog_icon);
+ permissionDialogIcon.setImageDrawable(icon);
+
+ final TextView permissionDialogTitle = content.findViewById(
+ R.id.accessibility_permissionDialog_title);
+ permissionDialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
+ getServiceName(context, info)));
+
+ final Button permissionAllowButton = content.findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ final Button permissionDenyButton = content.findViewById(
+ R.id.accessibility_permission_enable_deny_button);
+ permissionAllowButton.setOnClickListener(allowListener);
+ permissionAllowButton.setOnTouchListener(getTouchConsumingListener());
+ permissionDenyButton.setOnClickListener(denyListener);
+
+ final Button uninstallButton = content.findViewById(
+ R.id.accessibility_permission_enable_uninstall_button);
+ // Show an uninstall button to help users quickly remove non-preinstalled apps.
+ if (!info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) {
+ uninstallButton.setVisibility(View.VISIBLE);
+ uninstallButton.setOnClickListener(uninstallListener);
+ }
+ return content;
+ }
+
+ @VisibleForTesting
+ @SuppressLint("ClickableViewAccessibility") // Touches are intentionally consumed
+ public static View.OnTouchListener getTouchConsumingListener() {
+ return (view, event) -> {
+ // Filter obscured touches by consuming them.
+ if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
+ || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ Toast.makeText(view.getContext(),
+ R.string.accessibility_dialog_touch_filtered_warning,
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ return false;
+ };
+ }
+
+ // Get the service name and bidi wrap it to protect from bidi side effects.
+ private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
+ final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
+ final CharSequence label =
+ info.getResolveInfo().loadLabel(context.getPackageManager());
+ return BidiFormatter.getInstance(locale).unicodeWrap(label);
+ }
+}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 987c14c..d4eccd4 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -28,6 +28,7 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
@@ -56,7 +57,7 @@
"accessibility_shortcut_menu_mode";
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
private AlertDialog mMenuDialog;
- private AlertDialog mPermissionDialog;
+ private Dialog mPermissionDialog;
private ShortcutTargetAdapter mTargetAdapter;
@Override
@@ -123,7 +124,7 @@
if (target instanceof AccessibilityServiceTarget) {
showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
- mTargetAdapter);
+ position, mTargetAdapter);
return;
}
}
@@ -149,20 +150,43 @@
}
private void showPermissionDialogIfNeeded(Context context,
- AccessibilityServiceTarget serviceTarget, ShortcutTargetAdapter targetAdapter) {
+ AccessibilityServiceTarget serviceTarget, int position,
+ ShortcutTargetAdapter targetAdapter) {
if (mPermissionDialog != null) {
return;
}
- mPermissionDialog = new AlertDialog.Builder(context)
- .setView(createEnableDialogContentView(context, serviceTarget,
- v -> {
- mPermissionDialog.dismiss();
- targetAdapter.notifyDataSetChanged();
- },
- v -> mPermissionDialog.dismiss()))
- .setOnDismissListener(dialog -> mPermissionDialog = null)
- .create();
+ if (Flags.deduplicateAccessibilityWarningDialog()) {
+ mPermissionDialog = AccessibilityServiceWarning
+ .createAccessibilityServiceWarningDialog(context,
+ serviceTarget.getAccessibilityServiceInfo(),
+ v -> {
+ serviceTarget.onCheckedChanged(true);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ }, v -> {
+ serviceTarget.onCheckedChanged(false);
+ mPermissionDialog.dismiss();
+ },
+ v -> {
+ mTargets.remove(position);
+ context.getPackageManager().getPackageInstaller().uninstall(
+ serviceTarget.getComponentName().getPackageName(), null);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ });
+ mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
+ } else {
+ mPermissionDialog = new AlertDialog.Builder(context)
+ .setView(createEnableDialogContentView(context, serviceTarget,
+ v -> {
+ mPermissionDialog.dismiss();
+ targetAdapter.notifyDataSetChanged();
+ },
+ v -> mPermissionDialog.dismiss()))
+ .setOnDismissListener(dialog -> mPermissionDialog = null)
+ .create();
+ }
mPermissionDialog.show();
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 0f85075..51a5ddf 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -296,6 +296,10 @@
}
}
+ /**
+ * @deprecated Use {@link AccessibilityServiceWarning}.
+ */
+ @Deprecated
static View createEnableDialogContentView(Context context,
AccessibilityServiceTarget target, View.OnClickListener allowListener,
View.OnClickListener denyListener) {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6859f1f..76ae3e0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8391,6 +8391,10 @@
android:exported="true">
</provider>
+ <meta-data
+ android:name="com.android.server.patch.25239169"
+ android:value="true" />
+
</application>
</manifest>
diff --git a/core/res/res/drawable/ic_accessibility_generic.xml b/core/res/res/drawable/ic_accessibility_generic.xml
new file mode 100644
index 0000000..68a89e6
--- /dev/null
+++ b/core/res/res/drawable/ic_accessibility_generic.xml
@@ -0,0 +1,30 @@
+<!--
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M5.6875,22.8235C4.9092,22.4776 4.8184,22.2615 2.8752,16.1257 1.8439,12.8691 1.0015,10.0882 1.0033,9.946 1.0137,9.1246 1.3166,8.8389 6.25,4.9976 9.2052,2.6966 11.2442,1.1943 11.5332,1.1049 11.8724,0.9999 12.1235,0.996 12.432,1.0907 12.9214,1.2408 22.3634,8.7104 22.6857,9.2024 23.1266,9.8752 23.0768,10.1907 22.0053,13.5155 19.0153,22.7935 19.1481,22.461 18.2853,22.8286 17.7053,23.0757 6.2446,23.0711 5.6875,22.8235Z"
+ android:strokeWidth="0.31999999"
+ android:fillColor="#ced6da"/>
+ <path
+ android:pathData="M10.0615,19.3507C10.028,19.2609 9.9864,17.362 9.9691,15.1308L9.9375,11.0741 8.5,10.853c-2.1981,-0.3381 -2.1924,-0.3355 -2.1619,-0.978 0.0141,-0.2963 0.074,-0.587 0.1331,-0.6462 0.06,-0.06 0.7667,0.0113 1.5994,0.1614 2.1217,0.3824 5.7371,0.3824 7.8588,0 0.8206,-0.1479 1.5349,-0.2259 1.5874,-0.1733 0.0525,0.0526 0.1334,0.3334 0.1799,0.624 0.078,0.4881 0.0598,0.5378 -0.2384,0.6512 -0.1776,0.0675 -1.0143,0.2259 -1.8593,0.352l-1.5364,0.2293 -0.0625,4.182 -0.0625,4.182l-0.625,0 -0.625,0l-0.0625,-1.875 -0.0625,-1.875l-0.5625,0L11.4375,15.6875l-0.0625,1.875 -0.0625,1.875 -0.595,0.0382c-0.4038,0.0259 -0.6146,-0.0143 -0.6559,-0.125zM11.3716,8.912c-0.4861,-0.3351 -0.6133,-0.5622 -0.6176,-1.1029 -0.0047,-0.6005 0.2255,-0.9684 0.739,-1.1811 0.8994,-0.3726 1.7571,0.2075 1.7571,1.1885 0,0.4533 -0.0659,0.5905 -0.4418,0.9206 -0.5007,0.4396 -0.9697,0.4967 -1.4366,0.1749z"
+ android:strokeWidth="0.31999999"
+ android:fillColor="#ffffff"/>
+</vector>
diff --git a/core/res/res/layout/accessibility_service_warning.xml b/core/res/res/layout/accessibility_service_warning.xml
new file mode 100644
index 0000000..0381fac
--- /dev/null
+++ b/core/res/res/layout/accessibility_service_warning.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textDirection="locale"
+ android:scrollbarStyle="outsideOverlay"
+ android:gravity="top">
+
+ <LinearLayout
+ android:accessibilityDataSensitive="yes"
+ style="@style/AccessibilityDialog">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp">
+
+ <ImageView
+ android:id="@+id/accessibility_permissionDialog_icon"
+ style="@style/AccessibilityDialogServiceIcon" />
+
+ <TextView
+ android:id="@+id/accessibility_permissionDialog_title"
+ style="@style/AccessibilityDialogTitle" />
+
+ <TextView
+ android:id="@+id/permissionDialog_description"
+ android:text="@string/accessibility_service_warning_description"
+ style="@style/AccessibilityDialogDescription" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="24dp" >
+
+ <ImageView
+ android:id="@+id/controlScreen_icon"
+ android:src="@drawable/ic_visibility"
+ style="@style/AccessibilityDialogIcon" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/controlScreen_title"
+ android:text="@string/accessibility_service_screen_control_title"
+ style="@style/AccessibilityDialogPermissionTitle" />
+
+ <TextView
+ android:id="@+id/controlScreen_description"
+ android:text="@string/accessibility_service_screen_control_description"
+ style="@style/AccessibilityDialogPermissionDescription" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="24dp" >
+
+ <ImageView
+ android:id="@+id/performAction_icon"
+ android:src="@drawable/ic_pan_tool"
+ style="@style/AccessibilityDialogIcon" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/performAction_title"
+ android:text="@string/accessibility_service_action_perform_title"
+ style="@style/AccessibilityDialogPermissionTitle" />
+
+ <TextView
+ android:id="@+id/performAction_description"
+ android:text="@string/accessibility_service_action_perform_description"
+ style="@style/AccessibilityDialogPermissionDescription" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <!-- Buttons on bottom of dialog -->
+ <LinearLayout
+ style="@style/AccessibilityDialogButtonList">
+
+ <Space
+ style="@style/AccessibilityDialogButtonBarSpace"/>
+
+ <Button
+ android:id="@+id/accessibility_permission_enable_allow_button"
+ android:text="@string/accessibility_dialog_button_allow"
+ style="@style/AccessibilityDialogButton" />
+
+ <Button
+ android:id="@+id/accessibility_permission_enable_deny_button"
+ android:text="@string/accessibility_dialog_button_deny"
+ style="@style/AccessibilityDialogButton" />
+
+ <Button
+ android:id="@+id/accessibility_permission_enable_uninstall_button"
+ android:text="@string/accessibility_dialog_button_uninstall"
+ android:visibility="gone"
+ style="@style/AccessibilityDialogButton" />
+ </LinearLayout>
+ </LinearLayout>
+
+</ScrollView>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index cec83de..eed186a 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4701,6 +4701,13 @@
<string name="accessibility_dialog_button_allow">Allow</string>
<!-- String for the deny button in accessibility permission dialog. [CHAR LIMIT=10] -->
<string name="accessibility_dialog_button_deny">Deny</string>
+ <!-- String for the uninstall button in accessibility permission dialog. -->
+ <string name="accessibility_dialog_button_uninstall">Uninstall</string>
+ <!-- Warning shown when user input has been blocked due to another app overlaying screen
+ content. Since we don't know what the app is showing on top of the input target, we
+ can't verify user consent. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_dialog_touch_filtered_warning">An app is obscuring the permission
+ request so your response cannot be verified.</string>
<!-- Title for accessibility select shortcut menu dialog. [CHAR LIMIT=100] -->
<string name="accessibility_select_shortcut_menu_title">Tap a feature to start using it:</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 13d04e5..619ec31 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1560,4 +1560,87 @@
<!-- The default style for input method switch dialog -->
<style name="InputMethodSwitchDialogStyle" parent="AlertDialog.DeviceDefault">
</style>
+
+ <style name="AccessibilityDialog">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:divider">@*android:drawable/list_divider_material</item>
+ <item name="android:showDividers">middle</item>
+ </style>
+
+ <style name="AccessibilityDialogServiceIcon">
+ <item name="android:layout_width">36dp</item>
+ <item name="android:layout_height">36dp</item>
+ <item name="android:layout_marginTop">16dp</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ <item name="android:scaleType">fitCenter</item>
+ </style>
+
+ <style name="AccessibilityDialogIcon">
+ <item name="android:layout_width">18dp</item>
+ <item name="android:layout_height">18dp</item>
+ <item name="android:layout_marginEnd">12dp</item>
+ <item name="android:scaleType">fitCenter</item>
+ </style>
+
+ <style name="AccessibilityDialogTitle"
+ parent="@android:style/TextAppearance.DeviceDefault">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+ </style>
+
+ <style name="AccessibilityDialogDescription"
+ parent="@android:style/TextAppearance.DeviceDefault">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginTop">16dp</item>
+ <item name="android:layout_marginBottom">32dp</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="AccessibilityDialogPermissionTitle"
+ parent="@android:style/TextAppearance.DeviceDefault">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+ </style>
+
+ <style name="AccessibilityDialogPermissionDescription"
+ parent="@android:style/TextAppearance.DeviceDefault">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ </style>
+
+ <style name="AccessibilityDialogButtonBarSpace">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">0dp</item>
+ <item name="android:visibility">gone</item>
+ </style>
+
+ <style name="AccessibilityDialogButtonList">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:divider">@*android:drawable/list_divider_material</item>
+ <item name="android:showDividers">middle</item>
+ </style>
+
+ <style name="AccessibilityDialogButton"
+ parent="@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">56dp</item>
+ <item name="android:paddingEnd">8dp</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 16ad5c9..f3aa936 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3622,11 +3622,14 @@
<java-symbol type="string" name="accessibility_uncheck_legacy_item_warning" />
<java-symbol type="layout" name="accessibility_enable_service_warning" />
+ <java-symbol type="layout" name="accessibility_service_warning" />
<java-symbol type="id" name="accessibility_permissionDialog_icon" />
<java-symbol type="id" name="accessibility_permissionDialog_title" />
<java-symbol type="id" name="accessibility_permission_enable_allow_button" />
<java-symbol type="id" name="accessibility_permission_enable_deny_button" />
+ <java-symbol type="id" name="accessibility_permission_enable_uninstall_button" />
<java-symbol type="string" name="accessibility_enable_service_title" />
+ <java-symbol type="string" name="accessibility_dialog_touch_filtered_warning" />
<java-symbol type="layout" name="accessibility_shortcut_chooser_item" />
<java-symbol type="id" name="accessibility_shortcut_target_checkbox" />
@@ -3655,6 +3658,7 @@
<java-symbol type="drawable" name="ic_accessibility_color_inversion" />
<java-symbol type="drawable" name="ic_accessibility_color_correction" />
+ <java-symbol type="drawable" name="ic_accessibility_generic" />
<java-symbol type="drawable" name="ic_accessibility_hearing_aid" />
<java-symbol type="drawable" name="ic_accessibility_magnification" />
<java-symbol type="drawable" name="ic_accessibility_reduce_bright_colors" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 20d8d91..62d58b6 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -165,6 +165,7 @@
<!-- AccessibilityShortcutChooserActivityTest permissions -->
<uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY" />
+ <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<application
android:theme="@style/Theme"
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
index dd116b5..088b57f 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -44,14 +44,19 @@
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.AlertDialog;
import android.app.KeyguardManager;
+import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.graphics.Rect;
+import android.os.Bundle;
import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -62,6 +67,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.Flags;
import android.view.accessibility.IAccessibilityManager;
@@ -90,12 +96,17 @@
@RunWith(AndroidJUnit4.class)
public class AccessibilityShortcutChooserActivityTest {
private static final String ONE_HANDED_MODE = "One-Handed mode";
+ private static final String ALLOW_LABEL = "Allow";
private static final String DENY_LABEL = "Deny";
+ private static final String UNINSTALL_LABEL = "Uninstall";
private static final String EDIT_LABEL = "Edit shortcuts";
private static final String LIST_TITLE_LABEL = "Choose features to use";
private static final String TEST_LABEL = "TEST_LABEL";
- private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class");
+ private static final String TEST_PACKAGE = "TEST_LABEL";
+ private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(TEST_PACKAGE,
+ "class");
private static final long UI_TIMEOUT_MS = 1000;
+ private UiAutomation mUiAutomation;
private UiDevice mDevice;
private ActivityScenario<TestAccessibilityShortcutChooserActivity> mScenario;
private TestAccessibilityShortcutChooserActivity mActivity;
@@ -117,6 +128,10 @@
private IAccessibilityManager mAccessibilityManagerService;
@Mock
private KeyguardManager mKeyguardManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private PackageInstaller mPackageInstaller;
@Before
public void setUp() throws Exception {
@@ -125,6 +140,7 @@
assumeFalse("AccessibilityShortcutChooserActivity not supported on watch",
pm.hasSystemFeature(PackageManager.FEATURE_WATCH));
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mDevice.wakeUp();
when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo);
@@ -134,12 +150,15 @@
when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME);
when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(
anyInt())).thenReturn(new ParceledListSlice<>(
- Collections.singletonList(mAccessibilityServiceInfo)));
+ Collections.singletonList(mAccessibilityServiceInfo)));
when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
anyString(), anyInt(), anyInt())).thenReturn(true);
when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+ when(mPackageManager.getPackageInstaller()).thenReturn(mPackageInstaller);
+
TestAccessibilityShortcutChooserActivity.setupForTesting(
- mAccessibilityManagerService, mKeyguardManager);
+ mAccessibilityManagerService, mKeyguardManager,
+ mPackageManager);
}
@After
@@ -150,18 +169,12 @@
}
@Test
- public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() {
+ @RequiresFlagsDisabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() {
launchActivity();
openShortcutsList();
- // Performing the double-click is flaky so retry if needed.
- for (int attempt = 1; attempt <= 2; attempt++) {
- onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
- if (mDevice.wait(Until.hasObject(By.text(DENY_LABEL)), UI_TIMEOUT_MS)) {
- break;
- }
- }
-
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
onView(withText(DENY_LABEL)).perform(scrollTo(), click());
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -170,6 +183,50 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_allow_rowChecked() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(ALLOW_LABEL);
+
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_deny_rowNotChecked() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(DENY_LABEL);
+
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(UNINSTALL_LABEL);
+
+ verify(mPackageInstaller).uninstall(eq(TEST_PACKAGE), any());
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(TEST_LABEL)),
+ UI_TIMEOUT_MS)).isFalse();
+ }
+
+ @Test
public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent()
throws Exception {
when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
@@ -239,6 +296,18 @@
mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), UI_TIMEOUT_MS);
}
+ private void clickSystemDialogButton(String dialogButtonText) {
+ // Use UiAutomation to find the button because UiDevice struggles to find
+ // a UI element in a system dialog.
+ final AccessibilityNodeInfo button =
+ mUiAutomation.getRootInActiveWindow()
+ .findAccessibilityNodeInfosByText(dialogButtonText).stream()
+ .filter(AccessibilityNodeInfo::isClickable).findFirst().get();
+ final Rect bounds = new Rect();
+ button.getBoundsInScreen(bounds);
+ mDevice.click(bounds.centerX(), bounds.centerY());
+ }
+
/**
* Used for testing.
*/
@@ -246,12 +315,30 @@
AccessibilityShortcutChooserActivity {
private static IAccessibilityManager sAccessibilityManagerService;
private static KeyguardManager sKeyguardManager;
+ private static PackageManager sPackageManager;
public static void setupForTesting(
IAccessibilityManager accessibilityManagerService,
- KeyguardManager keyguardManager) {
+ KeyguardManager keyguardManager,
+ PackageManager packageManager) {
sAccessibilityManagerService = accessibilityManagerService;
sKeyguardManager = keyguardManager;
+ sPackageManager = packageManager;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (Flags.deduplicateAccessibilityWarningDialog()) {
+ // Setting the Theme is necessary here for the dialog to use the proper style
+ // resources as designated in its layout XML.
+ setTheme(R.style.Theme_DeviceDefault_DayNight);
+ }
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return sPackageManager;
}
@Override
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
new file mode 100644
index 0000000..b76dd51
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.R;
+import com.android.internal.accessibility.TestUtils;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Unit Tests for
+ * {@link com.android.internal.accessibility.dialog.AccessibilityServiceWarning}
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@RequiresFlagsEnabled(
+ android.view.accessibility.Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+public class AccessibilityServiceWarningTest {
+ private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService";
+ private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary";
+ private static final String A11Y_SERVICE_COMPONENT_NAME =
+ "fake.package/test.a11yservice.name";
+
+ private Context mContext;
+ private AccessibilityServiceInfo mAccessibilityServiceInfo;
+ private AtomicBoolean mAllowListener;
+ private AtomicBoolean mDenyListener;
+ private AtomicBoolean mUninstallListener;
+
+ @Rule
+ public final Expect expect = Expect.create();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mAccessibilityServiceInfo = TestUtils.createFakeServiceInfo(
+ A11Y_SERVICE_PACKAGE_LABEL,
+ A11Y_SERVICE_SUMMARY,
+ A11Y_SERVICE_COMPONENT_NAME,
+ /* isAlwaysOnService*/ false);
+ mAllowListener = new AtomicBoolean(false);
+ mDenyListener = new AtomicBoolean(false);
+ mUninstallListener = new AtomicBoolean(false);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_hasExpectedWindowParams() {
+ final AlertDialog dialog =
+ AccessibilityServiceWarning.createAccessibilityServiceWarningDialog(
+ mContext,
+ mAccessibilityServiceInfo,
+ null, null, null);
+ final Window dialogWindow = dialog.getWindow();
+ assertThat(dialogWindow).isNotNull();
+
+ expect.that(dialogWindow.getAttributes().privateFlags
+ & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS).isEqualTo(
+ SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ expect.that(dialogWindow.getAttributes().type).isEqualTo(TYPE_SYSTEM_DIALOG);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_hasExpectedServiceName() {
+ final TextView title = createDialogContentView().findViewById(
+ R.id.accessibility_permissionDialog_title);
+ assertThat(title).isNotNull();
+
+ assertThat(title.getText().toString()).contains(A11Y_SERVICE_PACKAGE_LABEL);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickAllow() {
+ final View allowButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ assertThat(allowButton).isNotNull();
+
+ allowButton.performClick();
+
+ expect.that(mAllowListener.get()).isTrue();
+ expect.that(mDenyListener.get()).isFalse();
+ expect.that(mUninstallListener.get()).isFalse();
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickDeny() {
+ final View denyButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_deny_button);
+ assertThat(denyButton).isNotNull();
+
+ denyButton.performClick();
+
+ expect.that(mAllowListener.get()).isFalse();
+ expect.that(mDenyListener.get()).isTrue();
+ expect.that(mUninstallListener.get()).isFalse();
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickUninstall() {
+ final View uninstallButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_uninstall_button);
+ assertThat(uninstallButton).isNotNull();
+
+ uninstallButton.performClick();
+
+ expect.that(mAllowListener.get()).isFalse();
+ expect.that(mDenyListener.get()).isFalse();
+ expect.that(mUninstallListener.get()).isTrue();
+ }
+
+ @Test
+ public void getTouchConsumingListener() {
+ final View allowButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ assertThat(allowButton).isNotNull();
+ final View.OnTouchListener listener =
+ AccessibilityServiceWarning.getTouchConsumingListener();
+
+ expect.that(listener.onTouch(allowButton, createMotionEvent(0))).isFalse();
+ expect.that(listener.onTouch(allowButton,
+ createMotionEvent(MotionEvent.FLAG_WINDOW_IS_OBSCURED))).isTrue();
+ expect.that(listener.onTouch(allowButton,
+ createMotionEvent(MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED))).isTrue();
+ }
+
+ private View createDialogContentView() {
+ return AccessibilityServiceWarning.createAccessibilityServiceWarningDialogContentView(
+ mContext,
+ mAccessibilityServiceInfo,
+ (v) -> mAllowListener.set(true),
+ (v) -> mDenyListener.set(true),
+ (v) -> mUninstallListener.set(true));
+ }
+
+ private MotionEvent createMotionEvent(int flags) {
+ MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[]{
+ new MotionEvent.PointerProperties()
+ };
+ MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[]{
+ new MotionEvent.PointerCoords()
+ };
+ return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1, props, coords,
+ 0, 0, 0, 0, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, flags);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 36e18b3..dedac55 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -604,7 +604,6 @@
mAodIconsBindHandle.dispose();
}
if (nic != null) {
- nic.setOnLockScreen(true);
final DisposableHandle viewHandle = NotificationIconContainerViewBinder.bind(
nic,
mAodIconsViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index c438e49..12de185 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -87,7 +87,6 @@
}
if (NotificationIconContainerRefactor.isEnabled) {
- nic.setOnLockScreen(true)
nicBindingDisposable?.dispose()
nicBindingDisposable =
NotificationIconContainerViewBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 0b6e400..748ad92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -92,6 +92,7 @@
private int mIndexOfFirstViewInShelf = -1;
private float mCornerAnimationDistance;
private float mActualWidth = -1;
+ private int mMaxIconsOnLockscreen;
private final RefactorFlag mSensitiveRevealAnim =
RefactorFlag.forView(Flags.SENSITIVE_REVEAL_ANIM);
private boolean mCanModifyColorOfNotifications;
@@ -136,6 +137,7 @@
Resources res = getResources();
mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+ mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
ViewGroup.LayoutParams layoutParams = getLayoutParams();
final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -271,6 +273,7 @@
}
private int getSpeedBumpIndex() {
+ NotificationIconContainerRefactor.assertInLegacyMode();
return mHostLayout.getSpeedBumpIndex();
}
@@ -280,6 +283,7 @@
*/
@VisibleForTesting
public void updateActualWidth(float fractionToShade, float shortestWidth) {
+ NotificationIconContainerRefactor.assertInLegacyMode();
final float actualWidth = mAmbientState.isOnKeyguard()
? MathUtils.lerp(shortestWidth, getWidth(), fractionToShade)
: getWidth();
@@ -290,6 +294,15 @@
mActualWidth = actualWidth;
}
+ private void setActualWidth(float actualWidth) {
+ if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+ setBackgroundWidth((int) actualWidth);
+ if (mShelfIcons != null) {
+ mShelfIcons.setActualLayoutWidth((int) actualWidth);
+ }
+ mActualWidth = actualWidth;
+ }
+
@Override
public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
super.getBoundsOnScreen(outRect, clipToParent);
@@ -467,12 +480,26 @@
final float fractionToShade = Interpolators.STANDARD.getInterpolation(
mAmbientState.getFractionToShade());
- final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf);
- updateActualWidth(fractionToShade, shortestWidth);
+
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ if (mAmbientState.isOnKeyguard()) {
+ float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1);
+ float shortestWidth = mShelfIcons.calculateWidthFor(numViews);
+ float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade);
+ setActualWidth(actualWidth);
+ } else {
+ setActualWidth(getWidth());
+ }
+ } else {
+ final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf);
+ updateActualWidth(fractionToShade, shortestWidth);
+ }
// TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
- mShelfIcons.setSpeedBumpIndex(getSpeedBumpIndex());
+ if (!NotificationIconContainerRefactor.isEnabled()) {
+ mShelfIcons.setSpeedBumpIndex(getSpeedBumpIndex());
+ }
mShelfIcons.calculateIconXTranslations();
mShelfIcons.applyIconStates();
for (int i = 0; i < getHostLayoutChildCount(); i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index f8bc0ee..a85c440 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -25,9 +25,7 @@
import com.android.internal.policy.SystemBarUtils
import com.android.internal.statusbar.StatusBarIcon
import com.android.internal.util.ContrastColorUtil
-import com.android.systemui.CoreStartable
import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
@@ -39,21 +37,15 @@
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
-import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.onConfigChanged
-import com.android.systemui.util.asIndenting
import com.android.systemui.util.kotlin.mapValuesNotNullTo
import com.android.systemui.util.kotlin.stateFlow
-import com.android.systemui.util.printCollection
import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.stopAnimating
import com.android.systemui.util.ui.value
-import dagger.Binds
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
@@ -134,6 +126,7 @@
): DisposableHandle {
return view.repeatWhenAttached {
lifecycleScope.launch {
+ view.setUseIncreasedIconScale(true)
launch {
viewModel.icons.bindIcons(
view,
@@ -229,7 +222,6 @@
->
FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
}
-
val failedBindings = mutableSetOf<String>()
val boundViewsByNotifKey = ArrayMap<String, Pair<StatusBarIconView, Job>>()
var prevIcons = NotificationIconsViewData()
@@ -237,64 +229,111 @@
val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
prevIcons = iconsData
+ // Lookup 1:1 group icon replacements
val replacingIcons: ArrayMap<String, StatusBarIcon> =
- iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, info) ->
- boundViewsByNotifKey[info.notifKey]?.first?.statusBarIcon
+ iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, notifKey) ->
+ boundViewsByNotifKey[notifKey]?.first?.statusBarIcon
}
- view.setReplacingIcons(replacingIcons)
-
- for (notifKey in iconsDiff.removed) {
- failedBindings.remove(notifKey)
- val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
- view.removeView(child)
- job.cancel()
- }
-
- val toAdd: Sequence<String> =
- iconsDiff.added.asSequence().map { it.notifKey } + failedBindings
- for ((idx, notifKey) in toAdd.withIndex()) {
- val sbiv = viewStore.iconView(notifKey)
- if (sbiv == null) {
- failedBindings.add(notifKey)
- continue
+ view.withIconReplacements(replacingIcons) {
+ // Remove and unbind.
+ for (notifKey in iconsDiff.removed) {
+ failedBindings.remove(notifKey)
+ val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
+ view.removeView(child)
+ job.cancel()
}
- // The view might still be transiently added if it was just removed and added again
- view.removeTransientView(sbiv)
- view.addView(sbiv, idx)
- boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
- boundViewsByNotifKey[notifKey] =
- Pair(
- sbiv,
- launch {
- launch { layoutParams.collect { sbiv.layoutParams = it } }
- bindIcon(notifKey, sbiv)
- },
- )
- }
- notifyBindingFailures(failedBindings)
-
- view.setChangingViewPositions(true)
-
- // Re-sort notification icons
- val expectedChildren =
- iconsData.visibleKeys.mapNotNull { boundViewsByNotifKey[it.notifKey]?.first }
- val childCount = view.childCount
- for (i in 0 until childCount) {
- val actual = view.getChildAt(i)
- val expected = expectedChildren[i]
- if (actual === expected) {
- continue
+ // Add and bind.
+ val toAdd: Sequence<String> = iconsDiff.added.asSequence() + failedBindings
+ for ((idx, notifKey) in toAdd.withIndex()) {
+ // Lookup the StatusBarIconView from the store.
+ val sbiv = viewStore.iconView(notifKey)
+ if (sbiv == null) {
+ failedBindings.add(notifKey)
+ continue
+ }
+ // The view might still be transiently added if it was just removed and added
+ // again
+ view.removeTransientView(sbiv)
+ view.addView(sbiv, idx)
+ boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
+ boundViewsByNotifKey[notifKey] =
+ Pair(
+ sbiv,
+ launch {
+ launch { layoutParams.collect { sbiv.layoutParams = it } }
+ bindIcon(notifKey, sbiv)
+ },
+ )
}
- view.removeView(expected)
- view.addView(expected, i)
- }
- view.setChangingViewPositions(false)
- view.setReplacingIcons(null)
+ // Set the maximum number of icons to show in the container. Any icons over this
+ // amount will render as an "overflow dot".
+ val maxIconsAmount: Int =
+ when (iconsData.limitType) {
+ LimitType.MaximumIndex -> {
+ iconsData.visibleIcons
+ .asSequence()
+ .take(iconsData.iconLimit)
+ .count { info -> info.notifKey in boundViewsByNotifKey }
+ }
+ LimitType.MaximumAmount -> {
+ iconsData.iconLimit
+ }
+ }
+ view.setMaxIconsAmount(maxIconsAmount)
+
+ // Track the binding failures so that they appear in dumpsys.
+ notifyBindingFailures(failedBindings)
+
+ // Re-sort notification icons
+ view.changeViewPositions {
+ val expectedChildren: List<StatusBarIconView> =
+ iconsData.visibleIcons.mapNotNull {
+ boundViewsByNotifKey[it.notifKey]?.first
+ }
+ val childCount = view.childCount
+ for (i in 0 until childCount) {
+ val actual = view.getChildAt(i)
+ val expected = expectedChildren[i]
+ if (actual === expected) {
+ continue
+ }
+ view.removeView(expected)
+ view.addView(expected, i)
+ }
+ }
+ }
+ // Recalculate all icon positions, to reflect our updates.
+ view.calculateIconXTranslations()
}
}
+ /**
+ * Track which groups are being replaced with a different icon instance, but with the same
+ * visual icon. This prevents a weird animation where it looks like an icon disappears and
+ * reappears unchanged.
+ */
+ // TODO(b/305739416): Ideally we wouldn't swap out the StatusBarIconView at all, and instead use
+ // a single SBIV instance for the group. Then this whole concept can go away.
+ private inline fun <R> NotificationIconContainer.withIconReplacements(
+ replacements: ArrayMap<String, StatusBarIcon>,
+ block: () -> R
+ ): R {
+ setReplacingIcons(replacements)
+ return block().also { setReplacingIcons(null) }
+ }
+
+ /**
+ * Any invocations of [NotificationIconContainer.addView] /
+ * [NotificationIconContainer.removeView] inside of [block] will not cause a new add / remove
+ * animation.
+ */
+ private inline fun <R> NotificationIconContainer.changeViewPositions(block: () -> R): R {
+ setChangingViewPositions(true)
+ return block().also { setChangingViewPositions(false) }
+ }
+
/** External storage for [StatusBarIconView] instances. */
fun interface IconViewStore {
fun iconView(key: String): StatusBarIconView?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index b11eca2c..9cb60d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -15,10 +15,13 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import android.content.res.Resources
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor
import javax.inject.Inject
@@ -35,9 +38,12 @@
iconsInteractor: AlwaysOnDisplayNotificationIconsInteractor,
keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ @Main resources: Resources,
shadeInteractor: ShadeInteractor,
) {
+ private val maxIcons = resources.getInteger(R.integer.max_notif_icons_on_aod)
+
/** Are changes to the icon container animated? */
val areContainerChangesAnimated: Flow<Boolean> =
combine(
@@ -67,7 +73,8 @@
val icons: Flow<NotificationIconsViewData> =
iconsInteractor.aodNotifs.map { entries ->
NotificationIconsViewData(
- visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+ visibleIcons = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+ iconLimit = maxIcons,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index 1560106..8484fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
import com.android.systemui.statusbar.notification.icon.domain.interactor.NotificationIconsInteractor
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -29,8 +30,23 @@
/** [NotificationIconsViewData] indicating which icons to display in the view. */
val icons: Flow<NotificationIconsViewData> =
interactor.filteredNotifSet().map { entries ->
+ var firstAmbient = 0
+ val visibleKeys = buildList {
+ for (entry in entries) {
+ entry.toIconInfo(entry.shelfIcon)?.let { info ->
+ add(info)
+ // NOTE: we assume that all ambient notifications will be at the end of the
+ // list
+ if (!entry.isAmbient) {
+ firstAmbient++
+ }
+ }
+ }
+ }
NotificationIconsViewData(
- visibleKeys = entries.mapNotNull { it.toIconInfo(it.shelfIcon) },
+ visibleKeys,
+ iconLimit = firstAmbient,
+ limitType = LimitType.MaximumIndex,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index c03a4a5..af37e49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -15,9 +15,12 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import android.content.res.Resources
import android.graphics.Rect
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
@@ -44,9 +47,12 @@
headsUpIconInteractor: HeadsUpNotificationIconInteractor,
keyguardInteractor: KeyguardInteractor,
notificationsInteractor: ActiveNotificationsInteractor,
+ @Main resources: Resources,
shadeInteractor: ShadeInteractor,
) {
+ private val maxIcons = resources.getInteger(R.integer.max_notif_static_icons)
+
/** Are changes to the icon container animated? */
val animationsEnabled: Flow<Boolean> =
combine(
@@ -77,7 +83,8 @@
val icons: Flow<NotificationIconsViewData> =
iconsInteractor.statusBarNotifs.map { entries ->
NotificationIconsViewData(
- visibleKeys = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+ visibleIcons = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+ iconLimit = maxIcons,
)
}
@@ -86,7 +93,7 @@
headsUpIconInteractor.isolatedNotification
.combine(icons) { isolatedNotif, iconsViewData ->
isolatedNotif?.let {
- iconsViewData.visibleKeys.firstOrNull { it.notifKey == isolatedNotif }
+ iconsViewData.visibleIcons.firstOrNull { it.notifKey == isolatedNotif }
}
}
.pairwise(initialValue = null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
index 867be84..e8756d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
@@ -23,16 +23,33 @@
/** Encapsulates the collection of notification icons present on the device. */
data class NotificationIconsViewData(
/** Icons that are visible in the container. */
- val visibleKeys: List<NotificationIconInfo> = emptyList(),
- /** Keys of icons that are "behind" the overflow dot. */
- val collapsedKeys: Set<String> = emptySet(),
- /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
- val forceShowDot: Boolean = false,
+ val visibleIcons: List<NotificationIconInfo> = emptyList(),
+ /** Limit applied to the [visibleIcons]; can be interpreted different based on [limitType]. */
+ val iconLimit: Int = visibleIcons.size,
+ /** How [iconLimit] is applied to [visibleIcons]. */
+ val limitType: LimitType = LimitType.MaximumAmount,
) {
+ // TODO(b/305739416): This can be removed once we are no longer looking up the StatusBarIconView
+ // instances from outside of the view-binder layer. Ideally, we would just use MaximumAmount,
+ // and apply it directly to the list of visibleIcons by truncating the list to that amount.
+ // At the time of this writing, we cannot do that because looking up the SBIV can fail, and so
+ // we need additional icons to fall-back to.
+ /** Determines how a limit to the icons is to be applied. */
+ enum class LimitType {
+ /** The [Int] limit is a maximum amount of icons to be displayed. */
+ MaximumAmount,
+ /**
+ * The [Int] limit is a maximum index into the
+ * [list of visible icons][NotificationIconsViewData.visibleIcons] to be displayed; any
+ * icons beyond that index should be omitted.
+ */
+ MaximumIndex,
+ }
+
/** The difference between two [NotificationIconsViewData]s. */
data class Diff(
/** Icons added in the newer dataset. */
- val added: List<NotificationIconInfo> = emptyList(),
+ val added: List<String> = emptyList(),
/** Icons removed from the older dataset. */
val removed: List<String> = emptyList(),
/**
@@ -44,7 +61,7 @@
* same group. A view binder can use this information for special animations for this
* specific change.
*/
- val groupReplacements: Map<String, NotificationIconInfo> = emptyMap(),
+ val groupReplacements: Map<String, String> = emptyMap(),
)
companion object {
@@ -56,23 +73,20 @@
new: NotificationIconsViewData,
prev: NotificationIconsViewData
): Diff {
- val added: List<NotificationIconInfo> =
- new.visibleKeys.filter {
- it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
- }
+ val prevKeys = prev.visibleIcons.asSequence().map { it.notifKey }.toSet()
+ val newKeys = new.visibleIcons.asSequence().map { it.notifKey }.toSet()
+ val added: List<String> = newKeys.mapNotNull { key -> key.takeIf { it !in prevKeys } }
val removed: List<NotificationIconInfo> =
- prev.visibleKeys.filter {
- it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
- }
+ prev.visibleIcons.filter { it.notifKey !in newKeys }
val groupsToShow: Set<IconGroupInfo> =
- new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
- val replacements: ArrayMap<String, NotificationIconInfo> =
+ new.visibleIcons.asSequence().map { it.groupInfo }.toSet()
+ val replacements: ArrayMap<String, String> =
removed
.asSequence()
.filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
.groupBy { it.groupInfo.groupKey }
.mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
- vs.takeIf { it.size == 1 }?.get(0)
+ vs.takeIf { it.size == 1 }?.get(0)?.notifKey
}
return Diff(added, removed.map { it.notifKey }, replacements)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 9e5fd95..00e78a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -141,8 +141,10 @@
private int mMaxStaticIcons;
private boolean mDozing;
private boolean mOnLockScreen;
- private boolean mOverrideIconColor;
+ private int mSpeedBumpIndex = -1;
+ private int mMaxIcons = Integer.MAX_VALUE;
+ private boolean mOverrideIconColor;
private boolean mIsStaticLayout = true;
private final HashMap<View, IconState> mIconStates = new HashMap<>();
private int mDotPadding;
@@ -153,7 +155,6 @@
private boolean mChangingViewPositions;
private int mAddAnimationStartIndex = -1;
private int mCannedAnimationStartIndex = -1;
- private int mSpeedBumpIndex = -1;
private int mIconSize;
private boolean mDisallowNextAnimation;
private boolean mAnimationsEnabled = true;
@@ -170,6 +171,7 @@
private View mIsolatedIconForAnimation;
private int mThemedTextColorPrimary;
private Runnable mIsolatedIconAnimationEndRunnable;
+ private boolean mUseIncreasedIconScale;
public NotificationIconContainer(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -436,18 +438,24 @@
if (numIcons == 0) {
return 0f;
}
- final float contentWidth =
- mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1);
- return getActualPaddingStart()
- + contentWidth
- + getActualPaddingEnd();
+ final float contentWidth;
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ contentWidth = mIconSize * numIcons;
+ } else {
+ contentWidth = mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1);
+ }
+ return getActualPaddingStart() + contentWidth + getActualPaddingEnd();
}
@VisibleForTesting
boolean shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount,
int maxVisibleIcons) {
- return speedBumpIndex != -1 && i >= speedBumpIndex
- && iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ return i >= maxVisibleIcons && iconAppearAmount > 0.0f;
+ } else {
+ return speedBumpIndex != -1 && i >= speedBumpIndex
+ && iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+ }
}
@VisibleForTesting
@@ -502,9 +510,8 @@
firstOverflowIndex = i;
mVisualOverflowStart = translationX;
}
- final float drawingScale = mOnLockScreen && view instanceof StatusBarIconView
- ? ((StatusBarIconView) view).getIconScaleIncreased()
- : 1f;
+
+ final float drawingScale = getDrawingScale(view);
translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
}
mIsShowingOverflowDot = false;
@@ -554,9 +561,26 @@
}
}
+ private float getDrawingScale(View view) {
+ final boolean useIncreasedScale = NotificationIconContainerRefactor.isEnabled()
+ ? mUseIncreasedIconScale
+ : mOnLockScreen;
+ return useIncreasedScale && view instanceof StatusBarIconView
+ ? ((StatusBarIconView) view).getIconScaleIncreased()
+ : 1f;
+ }
+
+ public void setUseIncreasedIconScale(boolean useIncreasedIconScale) {
+ if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+ mUseIncreasedIconScale = useIncreasedIconScale;
+ }
+
private int getMaxVisibleIcons(int childCount) {
- return mOnLockScreen ? mMaxIconsOnAod :
- mIsStaticLayout ? mMaxStaticIcons : childCount;
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ return mMaxIcons;
+ } else {
+ return mOnLockScreen ? mMaxIconsOnAod : mIsStaticLayout ? mMaxStaticIcons : childCount;
+ }
}
private float getLayoutEnd() {
@@ -673,9 +697,15 @@
}
public void setSpeedBumpIndex(int speedBumpIndex) {
+ NotificationIconContainerRefactor.assertInLegacyMode();
mSpeedBumpIndex = speedBumpIndex;
}
+ public void setMaxIconsAmount(int maxIcons) {
+ if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+ mMaxIcons = maxIcons;
+ }
+
public int getIconSize() {
return mIconSize;
}
@@ -740,6 +770,7 @@
* configured to. Depending on these values, the layout of the AOD icons change.
*/
public void setOnLockScreen(boolean onLockScreen) {
+ NotificationIconContainerRefactor.assertInLegacyMode();
mOnLockScreen = onLockScreen;
}
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index fff8f78..412aa9b 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -120,6 +120,7 @@
"io/Util.cpp",
"io/ZipArchive.cpp",
"link/AutoVersioner.cpp",
+ "link/FeatureFlagsFilter.cpp",
"link/ManifestFixer.cpp",
"link/NoDefaultResourceRemover.cpp",
"link/PrivateAttributeMover.cpp",
diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp
new file mode 100644
index 0000000..fdf3f74
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter.cpp
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#include "link/FeatureFlagsFilter.h"
+
+#include <string_view>
+
+#include "androidfw/IDiagnostics.h"
+#include "androidfw/Source.h"
+#include "util/Util.h"
+#include "xml/XmlDom.h"
+#include "xml/XmlUtil.h"
+
+using ::aapt::xml::Element;
+using ::aapt::xml::Node;
+using ::aapt::xml::NodeCast;
+
+namespace aapt {
+
+class FlagsVisitor : public xml::Visitor {
+ public:
+ explicit FlagsVisitor(android::IDiagnostics* diagnostics,
+ const FeatureFlagValues& feature_flag_values,
+ const FeatureFlagsFilterOptions& options)
+ : diagnostics_(diagnostics), feature_flag_values_(feature_flag_values), options_(options) {
+ }
+
+ void Visit(xml::Element* node) override {
+ std::erase_if(node->children,
+ [this](std::unique_ptr<xml::Node>& node) { return ShouldRemove(node); });
+ VisitChildren(node);
+ }
+
+ bool HasError() const {
+ return has_error_;
+ }
+
+ private:
+ bool ShouldRemove(std::unique_ptr<xml::Node>& node) {
+ if (const auto* el = NodeCast<Element>(node.get())) {
+ auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag");
+ if (attr == nullptr) {
+ return false;
+ }
+
+ bool negated = false;
+ std::string_view flag_name = util::TrimWhitespace(attr->value);
+ if (flag_name.starts_with('!')) {
+ negated = true;
+ flag_name = flag_name.substr(1);
+ }
+
+ if (auto it = feature_flag_values_.find(std::string(flag_name));
+ it != feature_flag_values_.end()) {
+ if (it->second.has_value()) {
+ if (options_.remove_disabled_elements) {
+ // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag"
+ return *it->second == negated;
+ }
+ } else if (options_.flags_must_have_value) {
+ diagnostics_->Error(android::DiagMessage(node->line_number)
+ << "attribute 'android:featureFlag' has flag '" << flag_name
+ << "' without a true/false value from --feature_flags parameter");
+ has_error_ = true;
+ return false;
+ }
+ } else if (options_.fail_on_unrecognized_flags) {
+ diagnostics_->Error(android::DiagMessage(node->line_number)
+ << "attribute 'android:featureFlag' has flag '" << flag_name
+ << "' not found in flags from --feature_flags parameter");
+ has_error_ = true;
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ android::IDiagnostics* diagnostics_;
+ const FeatureFlagValues& feature_flag_values_;
+ const FeatureFlagsFilterOptions& options_;
+ bool has_error_ = false;
+};
+
+bool FeatureFlagsFilter::Consume(IAaptContext* context, xml::XmlResource* doc) {
+ FlagsVisitor visitor(context->GetDiagnostics(), feature_flag_values_, options_);
+ doc->root->Accept(&visitor);
+ return !visitor.HasError();
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/link/FeatureFlagsFilter.h b/tools/aapt2/link/FeatureFlagsFilter.h
new file mode 100644
index 0000000..1d342a7
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter.h
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+#include "android-base/macros.h"
+#include "cmd/Util.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+struct FeatureFlagsFilterOptions {
+ // If true, elements whose featureFlag values are false (i.e., disabled feature) will be removed.
+ bool remove_disabled_elements = true;
+
+ // If true, `Consume()` will return false (error) if a flag was found that is not in
+ // `feature_flag_values`.
+ bool fail_on_unrecognized_flags = true;
+
+ // If true, `Consume()` will return false (error) if a flag was found whose value in
+ // `feature_flag_values` is not defined (std::nullopt).
+ bool flags_must_have_value = true;
+};
+
+// Looks for the `android:featureFlag` attribute in each XML element, validates the flag names and
+// values, and removes elements according to the values in `feature_flag_values`. An element will be
+// removed if the flag's given value is FALSE. A "!" before the flag name in the attribute indicates
+// a boolean NOT operation, i.e., an element will be removed if the flag's given value is TRUE. For
+// example, if the XML is the following:
+//
+// <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+// <permission android:name="FOO" android:featureFlag="!flag"
+// android:protectionLevel="normal" />
+// <permission android:name="FOO" android:featureFlag="flag"
+// android:protectionLevel="dangerous" />
+// </manifest>
+//
+// If `feature_flag_values` contains {"flag", true}, then the <permission> element with
+// protectionLevel="normal" will be removed, and the <permission> element with
+// protectionLevel="normal" will be kept.
+//
+// The `Consume()` function will return false if there is an invalid flag found (see
+// FeatureFlagsFilterOptions for customizing the filter's validation behavior). Do not use the XML
+// further if there are errors as there may be elements removed already.
+class FeatureFlagsFilter : public IXmlResourceConsumer {
+ public:
+ explicit FeatureFlagsFilter(FeatureFlagValues feature_flag_values,
+ FeatureFlagsFilterOptions options)
+ : feature_flag_values_(std::move(feature_flag_values)), options_(options) {
+ }
+
+ bool Consume(IAaptContext* context, xml::XmlResource* doc) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FeatureFlagsFilter);
+
+ const FeatureFlagValues feature_flag_values_;
+ const FeatureFlagsFilterOptions options_;
+};
+
+} // namespace aapt
diff --git a/tools/aapt2/link/FeatureFlagsFilter_test.cpp b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
new file mode 100644
index 0000000..53086cc
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ */
+
+#include "link/FeatureFlagsFilter.h"
+
+#include <string_view>
+
+#include "test/Test.h"
+
+using ::testing::IsNull;
+using ::testing::NotNull;
+
+namespace aapt {
+
+// Returns null if there was an error from FeatureFlagsFilter.
+std::unique_ptr<xml::XmlResource> VerifyWithOptions(std::string_view str,
+ const FeatureFlagValues& feature_flag_values,
+ const FeatureFlagsFilterOptions& options) {
+ std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDom(str);
+ FeatureFlagsFilter filter(feature_flag_values, options);
+ if (filter.Consume(test::ContextBuilder().Build().get(), doc.get())) {
+ return doc;
+ }
+ return {};
+}
+
+// Returns null if there was an error from FeatureFlagsFilter.
+std::unique_ptr<xml::XmlResource> Verify(std::string_view str,
+ const FeatureFlagValues& feature_flag_values) {
+ return VerifyWithOptions(str, feature_flag_values, {});
+}
+
+TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" />
+ </manifest>)EOF",
+ {{"flag", false}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="flag" />
+ </manifest>)EOF",
+ {{"flag", false}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="!flag" />
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="flag" />
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="!flag"
+ android:protectionLevel="normal" />
+ <permission android:name="FOO" android:featureFlag="flag"
+ android:protectionLevel="dangerous" />
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto children = root->GetChildElements();
+ ASSERT_EQ(children.size(), 1);
+ auto attr = children[0]->FindAttribute(xml::kSchemaAndroid, "protectionLevel");
+ ASSERT_THAT(attr, NotNull());
+ ASSERT_EQ(attr->value, "dangerous");
+}
+
+TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <application>
+ <provider />
+ <activity>
+ <layout android:featureFlag="!flag" />
+ </activity>
+ </application>
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto application = root->FindChild({}, "application");
+ ASSERT_THAT(application, NotNull());
+ auto activity = application->FindChild({}, "activity");
+ ASSERT_THAT(activity, NotNull());
+ auto maybe_removed = activity->FindChild({}, "layout");
+ ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <application>
+ <provider />
+ <activity>
+ <layout android:featureFlag="flag" />
+ </activity>
+ </application>
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto application = root->FindChild({}, "application");
+ ASSERT_THAT(application, NotNull());
+ auto activity = application->FindChild({}, "activity");
+ ASSERT_THAT(activity, NotNull());
+ auto maybe_removed = activity->FindChild({}, "layout");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag=" " />
+ </manifest>)EOF",
+ {{"flag", false}});
+ ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="flag" />
+ </manifest>)EOF",
+ {{"flag", std::nullopt}});
+ ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="unrecognized" />
+ </manifest>)EOF",
+ {{"flag", true}});
+ ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) {
+ auto doc = Verify(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="bar" />
+ <permission android:name="FOO" android:featureFlag="unrecognized" />
+ </manifest>)EOF",
+ {{"flag", std::nullopt}});
+ ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) {
+ auto doc = VerifyWithOptions(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="flag" />
+ </manifest>)EOF",
+ {{"flag", false}}, {.remove_disabled_elements = false});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) {
+ auto doc = VerifyWithOptions(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="flag" />
+ </manifest>)EOF",
+ {{"flag", std::nullopt}}, {.flags_must_have_value = false});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) {
+ auto doc = VerifyWithOptions(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+ <permission android:name="FOO" android:featureFlag="unrecognized" />
+ </manifest>)EOF",
+ {{"flag", true}}, {.fail_on_unrecognized_flags = false});
+ ASSERT_THAT(doc, NotNull());
+ auto root = doc->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+} // namespace aapt